summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-26 09:17:14 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-09-26 09:17:14 +0000
commit750f4a54cbce570c3d555bceadab68d1b25b47e7 (patch)
treefde3b8f76f7f9804473132ffa35758929b5d0b6b /CLAUDE.md
parentd4b00e59086e1afc69464164a907bcfddd560a50 (diff)
Facebook videos, and a muxed fallback so they can be fetched at all
Individual videos, which is the whole of what is reliably possible: Facebook pages publish no RSS, so there is no equivalent of subscribing to a YouTube channel. Flagged that before building rather than quietly delivering half. A shared link is recognised across the shapes Facebook actually produces -- watch, page videos with or without a slug, reels, fb.watch and /share/v codes, on www, m and web hosts -- and lands in its own "Saved Facebook videos" container. Its own, because the container carries the kind that decides how its episodes are fetched, and one row cannot be honest about two sources. The guid is prefixed per source too, so a Facebook and a YouTube video that happen to share a numeric id stay separate episodes. The og: tag parsing YouTube already did privately is now OpenGraph, used by both, per the standing instruction about two solutions to the same problem. It also learned to tolerate either attribute order and single quotes, which real pages use. The download seam stops claiming to be YouTube's: yt-dlp takes a URL and works out the site itself, which is exactly what made this cheap. YouTubeDownloadSource is ExtractedAudioSource and ChaquopyYouTubeSource is ChaquopyAudioSource. One real find, and the reason the first attempt failed on a device: Facebook publishes no audio-only stream at all, only pre-muxed sd/hd mp4, so "bestaudio[ext=m4a]/bestaudio[ext=webm]/bestaudio" resolved to nothing and yt-dlp reported "Requested format is not available". _FORMAT now falls back to a muxed mp4. That is still a single already-complete file, so the no-ffmpeg rule holds -- what must never appear there is bestvideo+bestaudio. Media3 plays an mp4's audio track without complaint, and the data model has been media-agnostic since phase 1 for this sort of reason. Verified end to end on a device against a real public Facebook video: resolved to its own container with the right kind and guid, fetched by yt-dlp, and played. This also stands as the first proof of the previous commit's fetch-then-play path against a genuine extractor rather than a direct URL, since YouTube itself is bot-blocked from this machine. Known gap, not fixed here: Facebook serves no og: tags to a non-browser at all -- checked with the app's own user agent -- so a shared video shows as "Video <id>". yt-dlp knows the real title, and YouTubeAudio.probe is still sitting unused in the reachability allowlist waiting for exactly this. Small follow-up. No permission changes, no schema change. 425 unit tests green, detekt and lint clean, coverage 64.90 -> 65.18. 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.md33
1 files changed, 33 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 3e41672..623ba1b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -104,6 +104,11 @@ claims three separate times across these repos.
queue, play, variable speed, sleep timer, retention, unsubscribe, Android Auto with a browsable
library and voice search. 207 unit tests plus instrumented tests covering the media browse tree.
+**Since Phase 2**, in response to a round of Peter's feedback: yt-dlp sources play (they are
+fetched first), playback errors are surfaced instead of being swallowed, anything queued is
+downloaded at once, Facebook videos are supported, the library ranks shows by their newest
+*unqueued* episode, and trailer/preview filter rules ship as defaults.
+
**Phase 2 is complete**, in three tracks. Peter deferred the audio-fingerprint skip detection
(the design plan's Tier 3), so the simple skip work came last.
@@ -149,6 +154,18 @@ out of a possibly-partial MP3, and the JSON path covers the shows that mark chap
title+date. An episode that loses its identity loses its playback position.
- **Retention is auto-delete-once-played** with a grace period, so a mis-tap doesn't destroy an
episode you were still listening to.
+- **A yt-dlp source has no playable URL until it is fetched.** A YouTube or Facebook episode's
+ `mediaUrl` is its watch page; the audio URL only exists once yt-dlp has resolved it.
+ `SubscriptionKind.needsExtraction` says so, `PlaybackAdvancer.start` reports `NeedsFetch`
+ rather than a `NextUp` that cannot work, and `fetchAndStart` does the fetch. Both the service
+ and the view model go through it -- the queue advances without the UI.
+
+- **Never `bestvideo+bestaudio` in `ytaudio.py`.** That is the form that needs ffmpeg, which is
+ not on Android. Every selector in `_FORMAT` picks one already-complete file. Audio-only comes
+ first, then a muxed mp4: **Facebook publishes no audio-only stream at all**, only pre-muxed
+ sd/hd, so the original audio-only-or-nothing selector failed with "Requested format is not
+ available". A muxed mp4 is still a single file and Media3 plays its audio track.
+
- **YouTube audio is yt-dlp, embedded.** Chaquopy runs an in-process CPython with real yt-dlp;
see `youtube/YouTubeAudio.kt` and `src/main/python/ytaudio.py`. No Termux, no podkit, no
server round trip.
@@ -191,6 +208,22 @@ out of a possibly-partial MP3, and the JSON path covers the shows that mark chap
alias and password, because that is the key already on the phone. Never swap it for a fresh
one — that costs another uninstall. Never use it for a release.
+- **Facebook is individual videos only.** Facebook pages publish no RSS, so there is no
+ equivalent of subscribing to a YouTube channel. Shared videos land in their own "Saved
+ Facebook videos" container, because the container carries the kind that decides how its
+ episodes are fetched and one row cannot be honest about two sources.
+
+- **Facebook tells a non-browser nothing.** It serves no `og:` tags at all -- verified with the
+ app's own user agent -- so a shared video gets a fallback title of "Video <id>". yt-dlp knows
+ the real title (`probe` in `ytaudio.py` returns it, and `YouTubeAudio.probe` is still sitting
+ in the reachability allowlist waiting for a caller). Backfilling from it is the obvious fix
+ and is not done yet.
+
+- **Anything entering the queue is downloaded immediately, on any network.** The hook is
+ `QueueRepository`'s `onQueued`, fired with newly-present ids only, because the queue is the
+ one choke point every route goes through. The one-off job deliberately has no network
+ constraint; only the periodic sweep is wifi-only.
+
- **Feeds are big.** A real subscription in testing produced 2753 episodes in one refresh. Any
per-episode work — a query in a loop, an O(n²) match — needs to be considered at that scale.