Commit graph

11 commits

Author SHA1 Message Date
Kayos
7894fe5a4d Straw phase M-2: MediaSession + audio focus + service skeleton
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.
2026-05-23 20:46:47 -07:00
Kayos
1578de5dbb Straw phase M-1: tap-thumbnail-to-play + hi-res thumbs + Picture-in-Picture
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).
2026-05-23 20:40:53 -07:00
Kayos
2fd439cac8 Straw audit-fix sprint: CRIT-1 + HIGH-9 + targeted MED
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
2026-05-23 20:23:34 -07:00
Kayos
01496c647a Straw phase K: subscriptions store + Subscribe button + Home section
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.
2026-05-23 20:02:52 -07:00
Kayos
06e6ec64e3 Straw phase J: tappable uploader → channel browse
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.
2026-05-23 19:58:37 -07:00
Kayos
6f5e1ed199 Straw phase I: SB segment count chip + clear-history buttons
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).
2026-05-23 19:54:37 -07:00
Kayos
ce3ba9afa2 Straw phase H: Settings screen + SponsorBlock category toggles
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.
2026-05-23 19:51:43 -07:00
Kayos
b3a0972909 Straw phase G: recent watches + search history (SharedPreferences-lite)
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.
2026-05-23 19:47:29 -07:00
Kayos
f3b78b4530 Straw phase F: visible polish — RYD, HTML, intent filter, network sec
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
2026-05-23 19:40:08 -07:00
Kayos
496ed30bda Sulkta day-2: search → detail → player → SponsorBlock + RYD
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.
2026-05-23 19:22:52 -07:00
Kayos
ff4dc6f121 Sulkta day-1: straw — KMP/Compose YouTube client fork
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.
2026-05-23 17:37:55 -07:00