vc=20 fixed channel videos but left two player rough edges that Cobb
called out on the phone:
* Tapping 🎧 (background audio) restarted the stream from the
beginning instead of picking up where the foreground player was.
* Pressing HOME on the player auto-entered Picture-in-Picture; what
Cobb wants is seamless background audio.
Both paths now share one handoff: capture exoPlayer.currentPosition,
stop the activity player, start PlaybackService with EXTRA_POSITION_MS,
and seekTo(position) on setMediaItem. Verified on emulator: 🎧 tap from
~34s in-track resumes service at position=34821ms.
HOME-on-player triggers the same path via StrawActivity.onUserLeaveHint
→ PlayerLeaveHandler.handler (a tiny registry the active PlayerScreen
registers in a DisposableEffect; cleared on dispose so the hook is a
no-op anywhere else in the app). The previous DisposableEffect that
called setAutoEnterEnabled(true) is gone; manual PiP via the ⊟ overlay
button stays — that one is still useful and user-triggered.
Also fixes a latent IllegalStateException that the new HOME path
exposed: PlaybackService and PlayerScreen both built MediaSession with
the default empty ID, which the system rejects when both live in the
same process. Service now sets .setId("straw-bg") so the two
sessions can coexist during the brief activity-vs-service overlap
during handoff.
Smoke (emulator 1440x3040):
* Subscriptions feed renders (vc=20 fix carries over).
* 🎧 from ~34s in NCS / Different Heaven → service playing
position=34821ms, no session-ID crash.
* HOME from PlayerScreen → focus moves to NexusLauncher,
PlaybackService running with isForeground=true,
pictureInPictureParams=null on the activity (no PiP).
vc=19 shipped with empty subscription feeds because
strawcore-core's channel_info was parsing the wrong tab + the
wrong renderer type.
strawcore-core e6fbbb7 fixes both — second-browse to the Videos
tab + parse lockupViewModel. This bump pulls that in.
NCS Spektrem + Rick Astley both play through Rust → ExoPlayer h264
MediaCodec on android-emulator. 4s frame-diff verified, zero
PlaybackException. Phase 7 + Phase 8 of the NPE port arc done.
Brings the Phase U Android-side integration onto a feature branch
where the rust wrapper now bridges to strawcore-core (the new NPE
port) instead of rustypipe.
* strawApp/build.gradle.kts — JNA dep + cargoBuild + cargoBuildHost
+ uniffiBindgen + the merge/compile task wiring. libs.newpipe
.extractor dropped.
* StrawApp.kt — uniffi.strawcore.initLogging(); NewPipe.init() gone
* 5 ViewModels (Search/VideoDetail/Player/Channel/Subscriptions)
swapped to call uniffi.strawcore.{search,streamInfo,channelInfo}
* PlayerScreen + PlaybackService adjusted for the new StreamInfo
shape (dash_mpd_url / hls_url instead of NPE's manifests)
* IosSafeHttpDataSource carried forward — strawcore-core gives us
Android-primary URLs so the iOS cap path is mostly dead code,
but kept as belt-and-suspenders for the rare HLS-fallback
* NewPipeDownloader.kt + util/Thumbnails.kt deleted
Files taken wholesale from sulkta branch — the wrapper's UniFFI
surface is identical between sulkta (rustypipe-backed) and the
current Phase-7-bridged-to-strawcore-core code, so Kotlin doesn't
care which extractor is under the hood.
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.
Path C (rustypipe iOS-client extractor) shipped on vc=16-17 returns
googlevideo URLs that the YT iOS-bound progressive-download path
deliberately caps at ~917 KiB end byte. Any seek past that returns 403,
making non-HLS videos unplayable. ExoPlayer's IosSafeHttpDataSource
chunking workaround in vc=17 doesn't help because the cap is on the
URL itself, not on the chunk size.
Rolling back to the vc=15 (0.1.0-AA) state — NewPipeExtractor — so
Cobb's phone auto-updates back to a working version while the
strawcore/rustypipe extractor strategy is re-thought (need a non-iOS
client that returns reliably-streamable URLs, OR a Lucy-side stitching
proxy that fetches iOS chunks server-side and re-serves DASH/HLS).
Branched from 5b36de888 (the vc=15 commit) — purely a versionCode +
versionName bump, no other changes.
Tap the 16:9 thumbnail box on VideoDetail and the video plays right
there in the card — like YouTube. Uses its own ExoPlayer (released on
nav-back via DisposableEffect) with PlayerView's built-in controls
(play/pause/seek/duration bar).
Top-right ⛶ pill on the inline player jumps to the existing fullscreen
PlayerScreen which still has the full toolset (speed picker, audio-only,
share, PiP, background, SponsorBlock chip). Restarts from 0 on entry —
seek-position handoff between inline + fullscreen is a future refinement.
Inline player state (playing/not-playing) is keyed on streamUrl so
navigating to a different video resets it back to the thumbnail-with-
play-overlay default.
Under the thumbnail: title → uploader → chips → buttons → description →
Recommended → More from <uploader>. The Z2/W2 inverted layout shipped in
Y put discovery cards before the title — bad UX, fixed back to standard.
Inline player coming next.
VideoDetail card order (top → bottom under the player thumbnail):
Recommended → More from <uploader> → Video details
NewPipeExtractor fan-out: streamInfo brings 'related', and a parallel
ChannelInfo+VIDEOS-tab fetch brings 'moreFromChannel' (filtered to drop
the current video). Same data shape as feed/search rows; reuses RelatedRow.
Home top bar gets a YouTube-style search pill in the title slot — tap
takes you to the search screen. The drawer 'Search' entry is gone (the
pill replaces it).
Section header below the top bar — 'Latest from your subs' / 'History' —
makes which view you're on obvious. Empty subs state is friendlier.
Status-bar padding (statusBarsPadding) added to VideoDetailScreen,
SearchScreen, ChannelScreen, SettingsScreen — fixes content rendering
under the Pixel camera cutout in edge-to-edge mode.
Phase U (rustypipe Rust extractor) rolled back. Symptom: black screen
on play, root cause: rustypipe 0.11.4's JS deobfuscator can't parse
current YouTube player.js (YT changed the obfuscation pattern, no
upstream rustypipe release since June 2025). Switching clients
(Web → TV → Android/Ios) didn't help — the deobfuscator init fires
universally.
Kept in place for the future:
- rust/strawcore/ Cargo workspace + UniFFI scaffolding
- crafting-table runtime install (rustup + 4 Android targets +
cargo-ndk + NDK r27c)
- The U-2..U-5 commits in history (re-runnable when rustypipe is
fixed or we fork it).
Restored from commit 9550b207a (v0.1.0-T):
- NewPipe.init() in StrawApp.onCreate
- libs.newpipe.extractor + libs.squareup.okhttp deps
- NewPipeDownloader.kt + Thumbnails.kt
- ViewModels (Search/VideoDetail/Player/Channel/SubscriptionFeed) on
NewPipeExtractor calls
- VideoDetailScreen Download dialog using NewPipe's StreamInfo
Future-direction memo: openclaw-workspace/memory/project_rustypipe_fork.md
— fork plan + revival path for the Rust extractor when we're ready
to maintain it.
Verified working in the Android emulator: dQw4w9WgXcQ plays, ExoPlayer
reports state=PLAYING(3), position advancing, video surface rendering.
Black-screen-on-play bug: rustypipe's default player() uses YT's Web
client, which serves stream URLs that are session/UA-locked. ExoPlayer
fetching with a different UA gets a silent 403 from googlevideo and
renders a black surface.
Fix: pin stream_info() to player_from_clients(id, [Tv, Ios]) — the
TVHTML5 + iOS Innertube clients serve direct-play URLs that work in
any HTTP player. Same trick NewPipe uses. No Apple/iOS code involved
— it's just the API client identifier rustypipe sends to YT.
Also added a Player.Listener in PlayerScreen that Toasts any
PlaybackException (codeName + message) so future stream failures don't
look like silent black screens. Logs to logcat 'StrawPlayer' too.
channel_info(url) UniFFI suspend fn. ChannelViewModel +
SubscriptionFeedViewModel both swap. NewPipeExtractor (Java) is OUT —
zero org.schabi.newpipe classes in the APK now.
Cleanup:
- NewPipeDownloader.kt deleted (was the OkHttp adapter)
- Thumbnails.kt deleted (rustypipe returns full URLs)
- NewPipe.init() dropped from StrawApp.onCreate
- libs.newpipe.extractor removed from build.gradle.kts
- STRAW_USER_AGENT + strawHttpClient() now live in net/Http.kt
- RydClient + SponsorBlockClient + PlayerScreen + PlaybackService all
read from net/Http.kt instead of the extractor package
rustypipe API quirks beat:
- channel_videos(id) is the right method (channel() doesn't exist)
- ChannelInfo struct = basic metadata; Channel<T> wrapper carries
name/avatar/banner + .content is the paginator of videos
- description is String (not Option), subscriber_count is Option<u64>
End state: strawApp Kotlin is ~UI + thin glue to strawcore. The Rust
core handles search / streamInfo / channel / channel_videos via UniFFI
suspend fns. Tokio + reqwest + rustls + rquickjs all packed in
libstrawcore.so (~6MB per ABI). APK 40MB total.
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
User-visible:
- BUG: 'clicking a second item went back to the first video' — VideoDetailViewModel
guard was short-circuiting on Activity-scoped ViewModel reuse. Now tracks
loadedUrl and only skips when the requested URL matches.
- BUG: PiP / window mode now auto-enters on Home gesture (Android 12+ via
setAutoEnterEnabled). Manual PiP button reports failure cause via Toast.
- HOME REDESIGN: replaced 3-tab bottom nav with hamburger ModalNavigationDrawer.
Default view = sub feed. Drawer items = Subscriptions / History / Search /
Settings. Top-left hamburger like normal Android apps.
Audit pass #2 (Opus max-effort) — CRIT + HIGH fixes shipped:
- CRIT-1: PlaybackService now calls startForeground() inside onStartCommand
with a media-playback notification + channel. Pre-fix could throw
ForegroundServiceDidNotStartInTimeException on Android 12+ and crash-kill.
- CRIT-2: AndroidManifest service exported=false. Previously any installed
app could craft an Intent and drive playback from attacker URLs.
- HIGH-1: 🎧 background handoff stops the activity player before starting
the service so we don't dual-host two ExoPlayers + MediaSessions.
- HIGH-2: onStartCommand returns START_NOT_STICKY and tears down on null
intent; no more crash-restart-crash loop after OS kills.
- HIGH-3: stop service on STATE_ENDED / STATE_IDLE via Player.Listener.
onTaskRemoved checks playbackState properly so we don't hold WAKE_LOCK
forever after a video ends in background.
- HIGH-4: Downloader validates scheme=https + googlevideo/youtube host
before handing the URL to DownloadManager.
- HIGH-5: filename sanitization extended to ASCII control chars, DEL,
Unicode bidi-override block, leading-dot, trailing whitespace.
- HIGH-6: SubscriptionFeedViewModel cancels prior in-flight refresh,
caps parallelism at 8 via Semaphore, applies 15s per-channel timeout.
- HIGH-7: sub feed error banner now shows above cached items when refresh
fails (previously hidden, looked indistinguishable from success).
- HIGH-8: PlayerViewModel falls back to lowest-available stream when no
stream is under the max-resolution ceiling (was: silent black screen).
- HIGH-9: network_security_config explicit cleartextTrafficPermitted='false'
on the RYD domain-config block (doesn't inherit from base-config).
- MED-1: PlaybackService.onDestroy nulls field before releasing session to
close a race with onGetSession during teardown.
- MED-6: Downloader catches enqueue exceptions, returns -1L, caller toasts
'download refused (bad URL)' instead of crashing.
Deferred (audit said 'can wait'): MED-2..5, MED-7..11, HIGH-10 UX consistency.
Phase P — bottom navigation:
- StrawHome restructured as a Scaffold with Material3 NavigationBar.
- Three tabs: Home (search + last 10 watches), Library (full watch
history with count), Subs (channel chips + aggregated feed).
Phase Q — subscription feed:
- New SubscriptionFeedViewModel fans out per-channel ChannelInfo +
ChannelTabs.VIDEOS fetches in parallel via async/awaitAll.
- Each channel contributes top 5; merged across all subs, capped at
200, sorted by view count as a soft-recency proxy (extractor doesn't
reliably surface upload timestamps).
- 10-minute cache TTL avoids hammering YT on tab re-entry.
- Subs tab renders the feed below the avatar row with a Refresh button.
Phase R — download:
- Download button on VideoDetail (next to Play / Share). Pops a tiny
dialog: Audio (best audioStream) or Video (best videoStream/
videoOnly fallback).
- Uses Android's DownloadManager — saves into app-private external
files dir (Android/data/com.sulkta.straw.debug/files/Movies/<kind>/).
Notification + progress for free. No WRITE_EXTERNAL_STORAGE needed.
- Filenames sanitized (no /:*?\"<>| chars), capped at 120 chars.
Phase S — background audio:
- New "Background" overlay button (🎧) on the player. Tap to pause the
activity player and start PlaybackService with the audio URL.
- PlaybackService is a Media3 MediaSessionService with its own ExoPlayer
configured with our custom DataSource.Factory (User-Agent set, cross-
protocol redirects). Foreground service + media notification.
- Audio survives activity death — swipe the app out of recents, audio
keeps playing. Stop via notification or open-the-app-and-tap-stop.
- onTaskRemoved keeps the service alive iff something is playing.
Versions shipped: P+Q as vc=4, R as vc=5, S as vc=6. Each landed in the
F-Droid repo for the day-by-day refresh path.
Day-N+ ideas: real MediaController unification (single Player for both
foreground + background paths), MergingMediaSource on the service side
for high-res YT videos, real upload-timestamp sort for feed once the
extractor exposes it consistently, queue/playlist.
VideoDetail screen:
- New "Related" section at the bottom — pulls
StreamInfo.relatedItems, filters to StreamInfoItem, renders as
inline thumbnail rows. Tap → push another VideoDetail. Up to 20
items shown. Each row uses bestThumbnail() for hi-res.
Settings screen + PlayerViewModel:
- New "Playback" section with a Max-Resolution picker:
Auto / 1080p / 720p / 480p / 360p / 144p. Persisted to
SharedPreferences (KEY_MAX_RES) via SettingsStore.maxResolution
StateFlow.
- PlayerViewModel.resolve filters videoStreams + videoOnlyStreams by
the ceiling before picking the max-bitrate one. Auto (Int.MAX_VALUE)
is unchanged behavior. Choosing 720p caps the renderer so 1080p/4K
streams are skipped — saves bandwidth on mobile + helps low-end
decoders.
Phase P next ideas: bottom navigation tabs (Home / Subs feed /
Library), Download (audio + video), the MediaSessionService refactor
for true background audio after activity death.
Player overlay (top-right) now hosts four buttons in a row:
- Speed: 1× by default; tap opens a dialog of 0.25× / 0.5× / 0.75× / 1× /
1.25× / 1.5× / 1.75× / 2×. Applies via exoPlayer.playbackParameters.
- Audio-only toggle (📻/📺): toggles Player.TRACK_TYPE_VIDEO via
TrackSelectionParameters. Saves bandwidth + battery for screen-off
listening. Toast confirms state.
- Share (↗): Intent.ACTION_SEND with text/plain containing the YouTube
URL + the video title as EXTRA_SUBJECT. Hands off to Android share
sheet.
- PiP (⊟): same as M-1 but now part of the row instead of its own
floating square.
VideoDetail screen: Play button now lives in a Row with an OutlinedButton
"Share" that fires the same ACTION_SEND chooser. Same UX surface for
users who land on detail without going to player.
Version: STRAW_VERSION_CODE 1 → 2, STRAW_VERSION_NAME "0.1.0-day1" →
"0.1.0-N" so the F-Droid client sees this as an upgrade.
Phase O next (per Cobb's "where are all the features"): quality picker,
related videos on detail, download (audio + video).
Wraps the existing Activity-owned ExoPlayer in a Media3 MediaSession. Side
effects:
- Lock-screen media controls (play/pause)
- System media notification with play/pause buttons
- Audio focus + ducking (other audio dims when straw plays)
- Bluetooth + headset hardware-button routing
- Plays nicely alongside other media apps' notifications
The ExoPlayer is also configured with AudioAttributes (USAGE_MEDIA +
CONTENT_TYPE_MOVIE) and handleAudioFocus=true so other apps can request
focus correctly (e.g., a phone call ducks/pauses straw).
Also scaffolded but NOT yet wired:
- PlaybackService extending MediaSessionService — the foundation for
true background-after-Activity-kill audio. Manifest entry + deps
added (media3-session 1.4.1, concurrent-futures-ktx 1.2.0). The
Activity-to-Service migration via MediaController is M-3 work.
- POST_NOTIFICATIONS, FOREGROUND_SERVICE,
FOREGROUND_SERVICE_MEDIA_PLAYBACK, WAKE_LOCK permissions in manifest.
For the user right now: lock the phone while a video plays — media
controls appear on the lock screen. Open another app — notification with
play/pause sits in the shade. Press a headset's play/pause — straw
responds. Pull the home screen — PiP keeps the video floating + audio
continues. Full screen-off-background-audio after killing the activity
arrives in M-3.
Cobb's real-use feedback: "playing a video is a button under a very
pixelated thumbnail, no playing in window mode, ... where are all the
features." This is the visible-UX pass.
Thumbnails:
- New util/Thumbnails.kt#bestThumbnail picks the highest-res image from
NewPipeExtractor's List<Image> by w*h pixel area. Was firstOrNull()
which is the smallest in the sorted list. Applied across Search,
Channel video rows, Channel avatar + banner, VideoDetail.
Tap-to-play:
- VideoDetail thumbnail is now wrapped in a clickable Box that fires
onPlay(). Centered semi-transparent black circle with a white play
triangle overlay so the affordance is obvious. The standalone "Play"
button below stays for accessibility (tap target consistency).
Picture-in-Picture:
- Manifest: android:supportsPictureInPicture="true" on StrawActivity +
added screenLayout|smallestScreenSize|screenSize to configChanges so
rotation into PiP doesn't recreate.
- PlayerScreen: top-right floating button enters PiP with 16:9 aspect.
- Lifecycle ON_STOP observer now checks isInPictureInPictureMode and
skips pausing when we're in PiP (was killing playback on PiP entry).
Phase M-2 next: MediaSession + background audio + foreground service
(the "play with screen off / lock screen controls" feature).
Phase L. Triages findings from the Opus max-effort audit
(memory/2026-05-23-night3-straw later; agent report inline).
CRIT-1: ACTION_SEND URL extracted from arbitrary text/plain share now
re-validated via URI host check before reaching NewPipeExtractor.
Manifest scheme also validated for VIEW. Regex broadened to accept
music.youtube.com + youtube-nocookie. Host set expanded.
HIGH: usesCleartextTraffic="true" removed from manifest (was redundant
with network_security_config + misleading to reviewers).
HIGH: NewPipeDownloader hardens header copy — runCatching around addHeader
to survive poisoned response headers with \r/\n. Explicit UA added AFTER
upstream header copy so it wins. Switched to OkHttp 5 extension form
for toRequestBody.
HIGH: Bounded response bodies via new util `cappedString(maxBytes)`. Caps
at 8MiB (NewPipeExtractor), 1MiB (SponsorBlock), 256KiB (RYD). Defends
against OOM via gigabyte response. Partial defense — chunked transfers
without Content-Length still streamed up to cap.
HIGH: HistoryStore / SettingsStore / SubscriptionsStore moved from
non-atomic `_flow.value = next` to `_flow.updateAndGet { ... }`. Closes
the read-modify-write race that could drop concurrent writes.
HIGH: VideoDetailViewModel.load had a dead-code `if (...)` block with no
body — falls through every call. Replaced with a real early-return on
detail-already-loaded.
HIGH: ChannelViewModel picked `info.tabs.firstOrNull()` which is YouTube's
curated "Home" tab. Switched to `firstOrNull { ChannelTabs.VIDEOS in
contentFilters }` with fallback. Channel screen now shows actual videos.
HIGH: PlayerScreen SponsorBlock skip loop hardened:
- dedup skipped UUIDs so re-listen doesn't fight the user
- poll 250ms → 150ms reduces sponsor leak through buffering
- `isPlaying` → `playbackState != IDLE/ENDED` so buffering doesn't miss
- clamp seek away from duration boundary (prevents past-end jank)
- filter POI-style point segments (start ≈ end)
MED: ExoPlayer pause on app background via Lifecycle.Event.ON_STOP
observer. Was silently playing audio with no MediaSession when app
was backgrounded.
MED: Log.d/Log.w gated behind BuildConfig.DEBUG via new strawLogD/strawLogW
inline helpers in util/Log.kt. Lambda body skipped in release too.
Log.i remains unguarded for user-quotable events. RydClient +
SponsorBlockClient + PlayerScreen updated.
MED: Hardcoded version "v0.1.0" in StrawHome replaced with
BuildConfig.VERSION_NAME. Now reads "v0.1.0-day1" matching ProjectConfig.
MED: Triplicated formatCount/formatViews/formatDuration extracted to
util/Formatting.kt. Search/Channel/VideoDetail import the shared
functions.
MED: SponsorBlockClient.buildJsonArray uses kotlinx-serialization Json
encoder instead of hand-rolled string concat — defends against future
user-typed category names breaking the URL.
MED: Description regex passes capped to 20k input chars before stripHtml
on detail screen — defends against ANR on multi-MB descriptions.
Deferred to phase M (not in this sprint):
- R8 + ProGuard rules for release builds (isMinifyEnabled=true)
- ExoPlayer hoisting into PlayerViewModel (AndroidViewModel)
- DI / Koin to replace `error("not initialized")` singleton accessors
- String resources / i18n
- rememberSaveable nav stack for process-death restore
- onNewIntent override for in-running YouTube URL shares
- certificate pinning on RYD endpoint
New SubscriptionsStore (SharedPrefs-lite, same pattern as History/Settings).
Holds Set<ChannelRef> { url, name, avatar }.
ChannelScreen header now has a "Subscribe / Subscribed" button on the
right. Button toggles membership; UI updates immediately via StateFlow.
StrawHome: new "Subscriptions" section above "Recently watched", a
LazyRow of subscribed channels (avatar + name, 80dp chips). Tap a
subscription chip → opens that channel.
StrawApp.onCreate: Subscriptions.init(this).
Day-4 ideas: auto-aggregate latest videos from subs on Home (no more
"hit search to see videos"), sub-feed-only screen, channel notifications.
New Screen.Channel(channelUrl, name). ChannelViewModel calls
NewPipeExtractor's ChannelInfo.getInfo() + first ChannelTab (Videos) for
the list of streams. ChannelScreen renders banner + circular avatar +
subscriber count + LazyColumn of recent videos.
Wiring:
- SearchViewModel.StreamItem now carries uploaderUrl from
StreamInfoItem.uploaderUrl.
- VideoDetailViewModel.VideoDetail likewise.
- VideoDetailScreen: uploader name is now a clickable Text in primary
color when uploaderUrl is non-null. Tap → Screen.Channel.
- StrawActivity routes Screen.Channel to ChannelScreen.
Smoke test: tapping "jawed" on the Me-at-the-zoo detail screen opens the
jawed channel — banner, avatar, 6.1M subscribers, the one video he ever
uploaded (this still cracks me up).
Day-4: tappable uploader in search row, channel tabs (Playlists, Shorts),
subscription toggle wired to a Subscriptions store.
Detail screen: VideoDetailViewModel now also fetches SponsorBlock segment
count for the user's currently-enabled categories alongside RYD. When >0,
detail screen shows a "⏭ N skip(s)" AssistChip next to the like/dislike
chips. Lets the user see at-a-glance whether SB is going to do anything
on this video before tapping Play.
Settings screen: "History" section at the bottom with two OutlinedButtons —
"Clear watch history" and "Clear searches". Each calls the corresponding
HistoryStore.clear* method. List updates immediately via the StateFlow.
Empty-category set on SB now means "skip the API call" (was already
checking this in PlayerViewModel.resolve, now also in VideoDetailViewModel).
New: SettingsStore (SharedPreferences-lite, same pattern as HistoryStore)
exposes a Set<SbCategory> StateFlow.
7 SponsorBlock categories surfaced as toggle rows in a new Settings screen:
sponsor (on by default), selfpromo, intro, outro, interaction (reminders),
music_offtopic (talking in music videos), filler.
Wiring:
- StrawApp.onCreate: Settings.init(this)
- StrawHome: new Settings gear icon next to "straw v0.1.0" header. Tap
routes to Screen.Settings.
- PlayerViewModel.resolve: reads Settings.get().sbCategories on resolve.
Empty set = SponsorBlock skip disabled (no API call).
Material-icons-core dep added (1.7.5) for the Icons.Filled.Settings glyph.
Day-4 ideas: theme override, default audio-only playback, preferred quality,
clear history buttons.
New: HistoryStore — JSON-encoded SharedPreferences for two lists. Watches
keyed by videoId (max 50, FIFO), searches deduped case-insensitively (max
20). Surfaced as StateFlow so screens recompose live.
Wiring:
- StrawApp.onCreate: History.init(this)
- VideoDetailViewModel.load: recordWatch after StreamInfo resolves
- SearchViewModel.submit: recordSearch on each query
UI:
- StrawHome: redesigned. Tight inline header, full-width Search button,
"Recently watched" LazyColumn below. Tap a recent row → VideoDetail.
- SearchScreen: when query is blank, show recent searches as
AssistChip row. Tap a chip → onQueryChange + submit (instant search).
Day-4 graduates to Room when there's a real query pattern that SP can't
serve (date ranges, full-text search). For now the JSON-blob approach
ships in 30 minutes vs an hour of Room+KSP plumbing.
Four polish items, all visible:
1. Fixed RYD URL: was returnyoutubedislike.com (the website, 404s on /votes)
→ returnyoutubedislikeapi.com (the actual API).
2. Bundled Sectigo "Public Server Authentication CA DV R36" intermediate
as an additional trust anchor (network_security_config.xml) for the
.com web host — the API uses Google Trust Services already so this
is defensive, in case we ever fetch the landing page.
3. Added intent-filter for YouTube URLs (VIEW + SEND) on StrawActivity.
Single-task launchMode; pickYouTubeUrl() routes the initial Intent
to Screen.VideoDetail(url) instead of Home.
4. stripHtml() utility removes <br>, </p>, and other tags from
NewPipeExtractor's description. Chapter timestamps now render
readably; raw HTML gone.
Also added Log.d to SponsorBlockClient and RydClient for visible
verification (StrawSb / StrawRyd tags). PlayerScreen now shows a
"SB: N segments" overlay chip in the top-left so the user can see
that SponsorBlock is armed.
Verified on Android 14 emulator with "Me at the zoo" intent:
- RYD: 👍 18.9M / 👎 424.2K renders
- Intent: direct URL → VideoDetail (no Home pass-through)
- HTML: chapter timestamps render clean
Phase A: NewPipeExtractor + OkHttp Downloader wired in. Search bar +
LazyColumn results. Tap = navigate to detail.
Phase B: VideoDetail screen — StreamInfo metadata + Return YouTube
Dislike chips + description.
Phase C: Media3 ExoPlayer in Compose. Resolves StreamInfo to best
playable: DASH MPD → HLS → combined progressive → merged
videoOnly+audio.
Phase D: SponsorBlock SHA-256 prefix lookup. 250ms position-poll loop
inside PlayerScreen — exoPlayer.seekTo(segment.end) when entering
a sponsor segment. Toast on skip.
Phase E: Verified live on Android 14 emulator. linus tech tips search
returns real results with thumbnails; tapped result opens detail;
hit Play → video plays through ExoPlayer.
Architecture: everything in :strawApp for now (not pushed into :shared
yet — KMP refactor is day-3). Pure-state nav (sealed Screen + stack,
no nav library).
Known polish gaps (day-3): RYD chips render empty on some videos,
description has raw HTML (markdown render needed), no Koin DI yet,
no persistence.
GPL-3.0-or-later per upstream NewPipe.
Initial Sulkta fork of NewPipe with a new :strawApp module that ships a
clean Compose-based Android APK at applicationId com.sulkta.straw.
What's here:
- :strawApp — thin Android application shell, MaterialTheme + StrawHome
Composable. Lives alongside legacy :app so we don't break upstream.
- buildSrc — STRAW_APPLICATION_ID/VERSION constants alongside the existing
NEWPIPE_APPLICATION_ID_OLD/NEW.
- docs/sulkta — RECON.md (NewPipe codebase breakdown) + DECISIONS.md
(stack + scope decisions).
NewPipe's :shared KMP scaffold is at 892 LOC and renders nothing; this
fork picks up there and races ahead. Day-1 ships a hello APK; day-2 wires
NewPipeExtractor + Media3 player + SponsorBlock + Return YouTube Dislike.
GPL-3.0-or-later per upstream.
Currently translated at 55.4% (51 of 92 strings)
Translated using Weblate (Latvian)
Currently translated at 97.8% (759 of 776 strings)
Translated using Weblate (Dutch)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Latvian)
Currently translated at 97.8% (759 of 776 strings)
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 66.3% (61 of 92 strings)
Translated using Weblate (Belarusian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 65.2% (60 of 92 strings)
Translated using Weblate (Slovak)
Currently translated at 77.1% (71 of 92 strings)
Translated using Weblate (Croatian)
Currently translated at 99.8% (775 of 776 strings)
Translated using Weblate (Serbian)
Currently translated at 99.2% (770 of 776 strings)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nero <guihsalmeida0210@hotmail.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: Yauhen <bugomol@users.noreply.hosted.weblate.org>
Co-authored-by: ojppe <ojppe@users.noreply.hosted.weblate.org>
Co-authored-by: Саша Петровић <salepetronije@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata
Move important version properties to buildSrc directory to access between modules
as needed.
Also add a simple task to generate a simple BuildConfig class to access version name.
This is better than adding dependency on a third-party library/plugin.
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Dutch)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Croatian)
Currently translated at 99.7% (774 of 776 strings)
Translated using Weblate (Spanish)
Currently translated at 72.8% (67 of 92 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Romanian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Azerbaijani)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Portuguese)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Slovak)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Italian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Turkish)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Latvian)
Currently translated at 97.1% (754 of 776 strings)
Translated using Weblate (Korean)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Greek)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Czech)
Currently translated at 100.0% (92 of 92 strings)
Translated using Weblate (Czech)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (French)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (French)
Currently translated at 100.0% (92 of 92 strings)
Translated using Weblate (Estonian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Persian)
Currently translated at 98.8% (767 of 776 strings)
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (92 of 92 strings)
Translated using Weblate (German)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Polish)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (German)
Currently translated at 100.0% (92 of 92 strings)
Translated using Weblate (Bulgarian)
Currently translated at 100.0% (776 of 776 strings)
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 63.7% (58 of 91 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 83.5% (76 of 91 strings)
Translated using Weblate (Spanish)
Currently translated at 72.5% (66 of 91 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (773 of 773 strings)
Translated using Weblate (Polish)
Currently translated at 100.0% (773 of 773 strings)
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (773 of 773 strings)
Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439jbyl80igqtf25uxnr0x1bg@users.noreply.hosted.weblate.org>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Edward <edwardchirita@mailbox.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicolás Pérez <nicoperez241@proton.me>
Co-authored-by: Olly <pifahif961@cadinr.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: UDP <udp@users.noreply.hosted.weblate.org>
Co-authored-by: VfBFan <vfbfan@users.noreply.hosted.weblate.org>
Co-authored-by: delvani <del.cidrak@users.noreply.hosted.weblate.org>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/en_GB/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hant/
Translation: NewPipe/Metadata