From ff4dc6f1219f47e92e53cbda45fe2008bd49cb62 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 23 May 2026 17:37:55 -0700 Subject: [PATCH] =?UTF-8?q?Sulkta=20day-1:=20straw=20=E2=80=94=20KMP/Compo?= =?UTF-8?q?se=20YouTube=20client=20fork?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- buildSrc/src/main/kotlin/ProjectConfig.kt | 5 + docs/sulkta/DECISIONS.md | 95 +++++++++++++ docs/sulkta/RECON.md | 130 ++++++++++++++++++ settings.gradle.kts | 3 +- strawApp/build.gradle.kts | 68 +++++++++ strawApp/src/main/AndroidManifest.xml | 23 ++++ .../kotlin/com/sulkta/straw/StrawActivity.kt | 33 +++++ .../main/kotlin/com/sulkta/straw/StrawHome.kt | 42 ++++++ 8 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 docs/sulkta/DECISIONS.md create mode 100644 docs/sulkta/RECON.md create mode 100644 strawApp/build.gradle.kts create mode 100644 strawApp/src/main/AndroidManifest.xml create mode 100644 strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt create mode 100644 strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index e07208970..baf5b07e4 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -13,3 +13,8 @@ const val NEWPIPE_VERSION_NAME = "0.28.7" const val NEWPIPE_APPLICATION_ID_OLD = "org.schabi.newpipe" const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app" + +// Sulkta fork — Straw +const val STRAW_VERSION_CODE = 1 +const val STRAW_VERSION_NAME = "0.1.0-day1" +const val STRAW_APPLICATION_ID = "com.sulkta.straw" diff --git a/docs/sulkta/DECISIONS.md b/docs/sulkta/DECISIONS.md new file mode 100644 index 000000000..f20497232 --- /dev/null +++ b/docs/sulkta/DECISIONS.md @@ -0,0 +1,95 @@ +# NewPipe-fork decisions — 2026-05-23 + +## Name: `straw` + +- Single short word, on-pattern with the rest of Sulkta's app family (cwho, + tny, hawk, adhdo, tny). +- Evokes "drawing content through a thin pipe" — fits the YouTube-extractor + metaphor without leaning on the "pipe" word (NewPipe already owns that). +- Easy to type, easy to read. + +If Cobb hates it, alts: `siphon`, `decant`, `funnel`, `kettle`. + +## Approach: fork NewPipe master, rebrand, build on `:shared` + +Not starting fresh. NewPipe has already done the gradle/CI/buildSrc/Koin/nav3 +scaffold work — we'd be reinventing wheels to redo it. Forking lets us: + +- Keep upstream as a remote, cherry-pick their improvements as they land. +- Build directly on `:shared` (the KMP module that has the scaffold but no + features). +- Keep the legacy `:app` module around as **reference for working extractor + wiring patterns** (search, channel, video resolve, player setup). Eventually + drop it once `:shared` is feature-complete. + +## Repo: `Sulkta-Coop/straw` on LAN Gitea + +- License: keep GPL-3.0-or-later (upstream's license, required for derivative). +- Branches: + - `master` — tracks upstream NewPipe master for clean rebase pulls. + - `sulkta` — our work branch, default branch for clones. +- Remote `upstream` pointing at `github.com:TeamNewPipe/NewPipe.git` for + periodic merges. + +## App identity + +| Field | Value | +|---|---| +| Application ID | `com.sulkta.straw` (replaces `net.newpipe.app`) | +| App name | "Straw" (debug suffix `Straw debug`) | +| Min SDK | 24 (Android 7, NewPipe is 23 — minor bump for Media3) | +| Target SDK | 35 (NewPipe matches) | +| Compile SDK | 36 (NewPipe matches) | +| Java toolchain | 21 (NewPipe matches) | +| Kotlin | 2.3.21 (NewPipe matches) | + +Legacy `:app` (id `org.schabi.newpipe`) stays unchanged for now — we don't +ship it but we read it. + +## Stack (matches NewPipe + 1 modernization) + +- **UI**: Jetpack Compose + Compose Multiplatform (commonMain Composables, + androidMain ComposeActivity host). +- **Nav**: androidx-navigation3 (type-safe sealed `Screen` interface). +- **DI**: Koin 4.x with `koin-plugin` annotation processor (`@KoinApplication`, + `@Single`, `@KoinViewModel`). +- **State**: ViewModels + StateFlow. +- **HTTP for SponsorBlock + RYD**: **Ktor 3.x client** (KMP-ready). NewPipe + uses OkHttp; we use Ktor because we want SB/RYD clients to live in + `commonMain` for future iOS/desktop. +- **Player**: **`androidx.media3` 1.4+** — NewPipe is still on legacy ExoPlayer + 2.19.1. Media3 is the upstream-replacement, better lifecycle, better DASH. +- **Image loading**: Coil 3 (NewPipe matches). +- **Serialization**: kotlinx-serialization (NewPipe matches). +- **Extractor**: `NewPipeExtractor v0.26.2` via JitPack (NewPipe matches). + JVM-only — lives in `androidMain` source set; `commonMain` declares an + expected interface, `androidMain` wires the actual extractor. + +## What we don't try to do on day 1 + +- **iOS target.** Extractor port is multi-week. iosApp stays a stub. +- **Desktop target.** Same constraint; no point until extractor ported. +- **History/subscriptions persistence.** Room is Android-only, fine for v0, + but we want SQLDelight (KMP) eventually. +- **Settings.** Use `:shared/SettingsModule` interface that already exists; + Android impl backed by DataStore. +- **F-Droid metadata.** Worry about it later. +- **Background audio + queue + autoplay.** Phase 2 work. +- **DRM / age-gated content.** Phase 2. + +## Day-1 deliverable scope + +A working APK with: + +1. **Search screen** — type a query, list video results from YouTube. Tap + opens video detail. +2. **Video detail screen** — title, channel, description, RYD like/dislike + row, "Play" button. +3. **Player screen** — Media3 ExoPlayer playing the resolved video stream. + SponsorBlock segments auto-skip during playback. Shows skip toast each + time. +4. **Settings sketch** — toggle for SponsorBlock categories (sponsor on by + default). + +That's the demo. **No** channel pages, subscriptions, history, downloads, +PiP, background audio, kiosks, comments, related videos. Those are Phase 2+. diff --git a/docs/sulkta/RECON.md b/docs/sulkta/RECON.md new file mode 100644 index 000000000..b839a1253 --- /dev/null +++ b/docs/sulkta/RECON.md @@ -0,0 +1,130 @@ +# NewPipe recon — 2026-05-23 + +Clone: `/root/build/newpipe-recon/` (shallow clone of upstream master 94005cf). + +## TL;DR + +NewPipe is **mid-migration to Kotlin Multiplatform + Compose Multiplatform**, but +the new app is at literal day-zero. Of 4 modules, only `:app` (the legacy +Android app) actually works. `:shared` has 892 LOC of scaffolding but zero +screens. `:desktopApp` is an 18-line stub. `:iosApp` has 0 lines of Kotlin. + +The "fork the legacy app and modernize it" angle is dead — they're modernizing. +The actual opening is: **pick up the abandoned-looking KMP scaffold and execute +the next year of work in a day's worth of vertical-slice ambition.** Bake in +SponsorBlock + RYD as first-class while we're at it. + +## Module breakdown + +| Module | LOC | Kt files | Java files | Status | +|---|---:|---:|---:|---| +| `:app` (legacy `NEWPIPE_APPLICATION_ID_OLD`) | 82,427 | 173 | 295 | What users run as v0.28.6 | +| `:shared` (KMP, `NEWPIPE_APPLICATION_ID_NEW`) | 892 | 21 | 0 | **Hello-world stage, zero screens** | +| `:desktopApp` | 18 | 1 | 0 | `Main.kt` stub only | +| `:iosApp` | 0 | 0 | 0 | Xcode shell only | + +### `:shared` current state + +`shared/src/commonMain/kotlin/net/newpipe/app/` contains: + +- `App.kt` — `@Composable fun App(...)` that sets up Koin + `AppTheme {}` with + an **empty body**. The KMP app literally renders nothing. +- `Constants.kt` — empty file-level constants. +- `navigation/NavDisplay.kt` — wraps androidx-nav3's `NavDisplay`, but + `entryProvider { }` block is empty. **No screens registered.** +- `navigation/Screen.kt` — sealed interface with **zero subclasses**. +- `di/KoinApp.kt` — bare `@KoinApplication object KoinApp` with no modules. +- `composable/TopAppBar.kt` — basic top bar. +- `theme/` — Color, ServiceTheme, Theme. Solid Material 3 scaffolding. +- `preview/ThemePreviewProvider.kt` — Compose preview helper. +- `di/settings/SettingsModule.kt` — multiplatform settings DI (separate + `commonMain` interface + `androidMain`/`jvmMain`/`iosMain` impls). + +That's it. The entire new app is theme + navigation primitive + Koin stub. + +### `:shared/androidMain` + +- `ComposeActivity.kt` — Android entry that hosts the Compose UI. +- `Constants.kt` — Android-specific constants. +- `extensions/Context.kt` — extension utilities. + +## Stack (already chosen by NewPipe, modern as of 2026) + +| Layer | Pick | Version | Notes | +|---|---|---|---| +| Language | Kotlin | 2.3.21 | Bleeding edge | +| UI | Jetpack Compose + Compose Multiplatform | 1.11.1 | Material3 1.11.0-alpha07 | +| Build | AGP | 9.2.1 | Latest | +| Nav | androidx-navigation3 | 1.1.1 | Type-safe, KMP-friendly | +| DI | Koin | 4.2.1 | Annotation-driven via koin-plugin | +| Async | kotlinx.coroutines | 1.11.0 | Plus kotlinx-coroutines-rx3 bridge to legacy | +| Serialization | kotlinx.serialization | 1.11.0 | JSON, parcelable replacement | +| Image loading | Coil 3 | 3.4.0 | KMP-native via coil-network-okhttp | +| HTTP | OkHttp | 5.3.2 | Android, used by extractor | +| DB | Room | 2.8.4 | Android-only, KMP-incompatible (legacy app) | +| Player | ExoPlayer 2.19.1 | — | **Stale.** Should be `androidx.media3` ≥ 1.4 | +| Crash | ACRA | 5.13.1 | Send-to-self crash reporter | +| Leak detection | LeakCanary | 2.14 | Debug builds only | + +## Critical dependency: `NewPipeExtractor` + +- `teamnewpipe-newpipe-extractor = "v0.26.2"` pulled from JitPack +- **JVM-only.** Uses Jsoup + nanojson + Java HTTP. +- Cannot live in `commonMain` for KMP. Lives in `:app` and would need + `androidMain` / `jvmMain` source sets in `:shared`. +- **This is why their KMP migration is slow** — porting the extractor to KMP + is the actual big yak. They haven't started it. + +For our fork: same constraint applies. Android-only target for day-1; KMP +extractor port is a separate (multi-week) project. + +## What's actively being deprecated + +NewPipe's own codebase has TODOs and migrations in flight: +- RxJava 3 → coroutines (bridges via `kotlinx-coroutines-rx3` show partial) +- ExoPlayer 2 → Media3 (still on legacy) +- Fragment + ViewBinding → Compose (the LeakScope findings live in this layer) +- Material 1 → Material 3 (mid-migration; `material = "1.11.0" # TODO: update`) +- KSP2 incompatibility flagged on `statesaver` +- Apache commons in `app/src/main/java/org/apache/commons/` — vendored, would + go away in KMP world + +## Where to hook SponsorBlock + Return YouTube Dislike + +For a from-scratch new app: + +- **SponsorBlock**: ExoPlayer/Media3 player listener that consumes a list of + segments fetched at video resolution time. Skip via `player.seekTo()` on + `onPositionDiscontinuity`/period-update callbacks. API: SHA-256 prefix + lookup at `sponsor.ajay.app/api/skipSegments/`. +- **Return YouTube Dislike**: HTTP GET to + `returnyoutubedislike.com/votes?videoId=`. Render in video detail screen + next to the like count. No player integration needed. + +Both clients can live in `:shared/commonMain` using **Ktor client** (KMP HTTP). +That makes them iOS/desktop-ready once an extractor port exists. + +## Existing forks doing the same thing + +Surfaced from NewPipe issue thread #13512: +- **PipePipe** — fork with SponsorBlock + RYD. Active. +- **Tubular** — fork with SponsorBlock + RYD. Active. +- **YouPipe** — fork with extra features. +- **SkyTube** — separate Android YouTube app, not NewPipe-based. + +So this isn't novel territory. We're doing it for the build experience +(element-x energy), not to fill a market gap. + +## Differentiators we could pursue + +If we want to be more than "another fork": + +1. **Compose-native from day 1** (vs. PipePipe/Tubular which are still on + Fragment+XML legacy). Smaller, faster, easier to maintain. +2. **Media3 ExoPlayer** (vs. NewPipe's legacy ExoPlayer 2). +3. **Ktor-based clients** for SB/RYD in `commonMain` — KMP-ready for the day + someone ports the extractor. +4. **First-class settings sync** via our existing Sulkta auth (Authentik OIDC) + for cross-device subscriptions/history. None of the existing forks do this. +5. **Sulkta-Coop signing cert** — joins the rest of our app family with + consistent signature. diff --git a/settings.gradle.kts b/settings.gradle.kts index 1b616793f..45bf0a6f6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,7 +20,8 @@ dependencyResolutionManagement { maven(url = "https://repo.clojars.org") } } -include (":app") // androidApp +include (":app") // androidApp (legacy NewPipe) +include(":strawApp") // Sulkta fork — Straw Android app shell include(":desktopApp") include("shared") diff --git a/strawApp/build.gradle.kts b/strawApp/build.gradle.kts new file mode 100644 index 000000000..da46f7b54 --- /dev/null +++ b/strawApp/build.gradle.kts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2026 Sulkta-Coop + * SPDX-License-Identifier: GPL-3.0-or-later + * + * :strawApp — thin Android application shell around :shared (the KMP Compose + * code). Lives alongside the legacy :app module so we don't break it. + */ + +import com.android.build.api.dsl.ApplicationExtension + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.compose) + alias(libs.plugins.jetbrains.kotlinx.serialization) +} + +kotlin { + jvmToolchain(21) +} + +configure { + compileSdk { + version = release(NEWPIPE_VERSION_SDK_COMPILE_MAJOR) { + minorApiLevel = NEWPIPE_VERSION_SDK_COMPILE_MINOR + } + } + namespace = STRAW_APPLICATION_ID + + defaultConfig { + applicationId = STRAW_APPLICATION_ID + minSdk { version = release(24) } + targetSdk { version = release(NEWPIPE_VERSION_SDK_TARGET) } + versionCode = STRAW_VERSION_CODE + versionName = STRAW_VERSION_NAME + resValue("string", "app_name", "Straw") + } + + buildTypes { + debug { + isDebuggable = true + applicationIdSuffix = ".debug" + resValue("string", "app_name", "Straw debug") + } + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + buildFeatures { + compose = true + buildConfig = true + resValues = true + } +} + +dependencies { + implementation(libs.androidx.activity) + implementation(libs.androidx.core) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.material3) + implementation(libs.jetbrains.compose.ui) +} diff --git a/strawApp/src/main/AndroidManifest.xml b/strawApp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6d4c7aec8 --- /dev/null +++ b/strawApp/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt new file mode 100644 index 000000000..01acef496 --- /dev/null +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 Sulkta-Coop + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.sulkta.straw + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.Modifier +import androidx.compose.foundation.layout.fillMaxSize + +class StrawActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContent { + val scheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + MaterialTheme(colorScheme = scheme) { + Surface(modifier = Modifier.fillMaxSize()) { + StrawHome() + } + } + } + } +} diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt new file mode 100644 index 000000000..24b16fcb8 --- /dev/null +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Sulkta-Coop + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.sulkta.straw + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun StrawHome() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "straw", + style = MaterialTheme.typography.displayLarge, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "v0.1.0-day1 — Sulkta-Coop", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +}