Round-7 Opus audits explicitly flagged the audit loop as past
diminishing returns. Landing the few real items + one race-order
fix; leaving the comment-hygiene tail for the next time someone
reads the files.
MED
R7-1 SearchViewModel cache-preview race: vc=41 wrote the cached
preview to UI before cancelling the prior inFlight, so the
prior coroutine (already past its ensureActive() gate)
could reach its terminal _ui.update and clobber the cache
preview. Moved inFlight?.cancel() above the preview write.
R7-2 StrawActivity.YT_HOSTS duplicated util.YtUrl.ALLOWED_YT_HOSTS
— drift risk where one gets a new host and the other
doesn't. Collapsed StrawActivity.looksLikeYouTube to call
util.isAllowedYtUrl, picking up the scheme + trailing-dot
defenses for free.
R7-3 Dropped dead once_cell dep from rust/strawcore/Cargo.toml.
Round-4's runtime rewrite uses AtomicBool + Mutex; nothing
consumes once_cell anymore.
Audit explicitly called out (and we agree) these as past the value
threshold for further rounds:
- Verified-clean: try_lock no deadlock, ensureActive fence, bad-URL
early-return cancel, duplicate-zip-entry rejection, LIMIT clauses,
SponsorBlock 50ms exclusion drop, applied++ count.
- False positive: H1 "Related videos always empty" — the section
already gates on `d.related.isNotEmpty()`; UI doesn't paint when
extractor stub returns empty.
- Deferred: R8 enable, Nav rememberSaveable, LazyColumn keys,
collectAsStateWithLifecycle, DASH/HLS max-resolution cap,
Gradle cargo-build input/output declarations (CI cost, not
runtime), legacy :app module trim, stale comment cleanup.
rquickjs-sys 0.11 ships pre-generated bindings only for x86_64 hosts;
Android targets need bindgen at build time. Direct-dep with the feature
flag so cargo's feature resolver lifts it to the transitive use through
strawcore-core → rquickjs → rquickjs-sys.
Replaces the rustypipe-backed extraction with calls into the new
NPE-port crate. The UniFFI surface Kotlin sees is unchanged:
suspend fun search(query: String): List<SearchItem>
suspend fun streamInfo(input: String): StreamInfo
suspend fun channelInfo(input: String): ChannelInfo
fun initLogging() // also wires the strawcore-core Downloader
fun helloFromRust(name: String): String
rust/strawcore/
* Cargo.toml — dropped rustypipe + rquickjs-sys direct dep;
added strawcore-core path dep (../../../strawcore)
* src/error.rs — From<strawcore_core::ExtractionError>, mapping
ContentUnavailable variants to typed
StrawcoreError cases (AgeRestricted, GeoRestricted,
Private, RequiresLogin) instead of bucketing all
to Extractor
* src/runtime.rs — Once-guarded ReqwestDownloader init via
NewPipe::init_full
* src/search.rs — search() spawn_blocks core search_extractor::search
against SearchFilter::Videos
* src/stream.rs — stream_info() resolves URL → video_id via
strawcore_core::linkhandler::stream, then
spawn_blocks core stream_extractor::stream_info,
then maps StreamInfo → wrapper DTOs (combined/
video_only/audio_only/dash/hls)
* src/channel.rs — channel_info() parses input via
strawcore_core::linkhandler::channel (handle /
custom-url / legacy-user resolution lives in
core), then spawn_blocks core channel::channel_info
Build verified: wrapper compiles linking strawcore-core, uniffi-bindgen
generates Kotlin bindings with the same suspend fun + data class
surface Kotlin already consumes. Android NDK cross-compile + APK + on-
device smoke pending (needs crafting-table container).
This commits onto rollback/vc18-back-to-NPE — the existing Kotlin code
still calls NewPipeExtractor directly. Switching the Kotlin side to
consume the rust wrapper is a separate cutover.
stream_info(url) UniFFI suspend fn replaces NewPipeExtractor's
StreamInfo.getInfo() for both VideoDetailViewModel and PlayerViewModel.
One Rust round-trip drives the detail screen render AND the player's
resolve(). The VideoDetailUiState.info field cached on detail load is
reused by the Download dialog so we don't refetch.
Deferred to U-3.5:
- like_count (rustypipe's player() doesn't surface engagement data;
a separate query is needed)
- related (player() doesn't include 'up next'; comes from a separate
endpoint). Kotlin gets empty list for now — RelatedRow handles it.
Type quirks vs my initial guesses (caught by cargo check):
- details.duration is u32, not Option<u32>
- channel is split into channel_id + channel_name, not a struct
- like_count doesn't exist at this query depth
- VideoFormat::Webm (lowercase mb), VideoCodec::Avc1 (not H264)
- video_only is a separate vec (video_only_streams), not a bool flag