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.
This commit is contained in:
parent
94005cfe9c
commit
ff4dc6f121
8 changed files with 398 additions and 1 deletions
|
|
@ -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"
|
||||
|
|
|
|||
95
docs/sulkta/DECISIONS.md
Normal file
95
docs/sulkta/DECISIONS.md
Normal file
|
|
@ -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+.
|
||||
130
docs/sulkta/RECON.md
Normal file
130
docs/sulkta/RECON.md
Normal file
|
|
@ -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/<prefix>`.
|
||||
- **Return YouTube Dislike**: HTTP GET to
|
||||
`returnyoutubedislike.com/votes?videoId=<id>`. 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.
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
68
strawApp/build.gradle.kts
Normal file
68
strawApp/build.gradle.kts
Normal file
|
|
@ -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<ApplicationExtension> {
|
||||
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)
|
||||
}
|
||||
23
strawApp/src/main/AndroidManifest.xml
Normal file
23
strawApp/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:icon="@android:drawable/sym_def_app_icon"
|
||||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".StrawActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
33
strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt
Normal file
33
strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt
Normal file
42
strawApp/src/main/kotlin/com/sulkta/straw/StrawHome.kt
Normal file
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue