diff --git a/strawApp/src/main/AndroidManifest.xml b/strawApp/src/main/AndroidManifest.xml
index 603cf8b03..7c9b0c3a4 100644
--- a/strawApp/src/main/AndroidManifest.xml
+++ b/strawApp/src/main/AndroidManifest.xml
@@ -11,15 +11,33 @@
android:roundIcon="@android:drawable/sym_def_app_icon"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
+ android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt
index bb0471331..376b3f0f4 100644
--- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawActivity.kt
@@ -5,6 +5,7 @@
package com.sulkta.straw
+import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
@@ -22,15 +23,23 @@ import com.sulkta.straw.feature.detail.VideoDetailScreen
import com.sulkta.straw.feature.player.PlayerScreen
import com.sulkta.straw.feature.search.SearchScreen
+private val YT_HOSTS = setOf("youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be")
+private val YT_URL_RE = Regex("https?://(?:www\\.|m\\.)?(?:youtube\\.com/[^\\s]+|youtu\\.be/[A-Za-z0-9_-]+)")
+
class StrawActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
+
+ val startUrl = pickYouTubeUrl(intent)
+
setContent {
val scheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
MaterialTheme(colorScheme = scheme) {
Surface(modifier = Modifier.fillMaxSize()) {
- val nav = rememberNavigator()
+ val initial: Screen =
+ if (startUrl != null) Screen.VideoDetail(startUrl, "") else Screen.Home
+ val nav = rememberNavigator(initial)
DisposableEffect(nav) {
val cb = object : OnBackPressedCallback(true) {
@@ -70,4 +79,26 @@ class StrawActivity : ComponentActivity() {
}
}
}
+
+ /** Pull a YouTube URL out of an incoming Intent (VIEW or SEND). */
+ private fun pickYouTubeUrl(intent: Intent?): String? {
+ intent ?: return null
+ when (intent.action) {
+ Intent.ACTION_VIEW -> {
+ val data = intent.data?.toString() ?: return null
+ if (looksLikeYouTube(data)) return data
+ }
+ Intent.ACTION_SEND -> {
+ val shared = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return null
+ val match = YT_URL_RE.find(shared) ?: return null
+ return match.value
+ }
+ }
+ return null
+ }
+
+ private fun looksLikeYouTube(url: String): Boolean {
+ val host = runCatching { java.net.URI(url).host }.getOrNull() ?: return false
+ return host.lowercase() in YT_HOSTS
+ }
}
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt
index 62e15073e..bda85ce24 100644
--- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/detail/VideoDetailScreen.kt
@@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
+import com.sulkta.straw.util.stripHtml
@Composable
fun VideoDetailScreen(
@@ -123,7 +124,7 @@ fun VideoDetailScreen(
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = d.description.take(2000),
+ text = stripHtml(d.description).take(2000),
style = MaterialTheme.typography.bodySmall,
)
}
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt
index d2ee7ff5b..cfdd49988 100644
--- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/player/PlayerScreen.kt
@@ -8,11 +8,14 @@
package com.sulkta.straw.feature.player
+import android.util.Log
import android.widget.Toast
import androidx.annotation.OptIn
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -23,6 +26,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -109,6 +114,7 @@ fun PlayerScreen(
val posSec = exoPlayer.currentPosition / 1000.0
val segment = pickActiveSegment(segments, posSec)
if (segment != null) {
+ Log.i("StrawSb", "skip: ${segment.category} ${segment.startSec}s..${segment.endSec}s (pos=$posSec)")
exoPlayer.seekTo((segment.endSec * 1000).toLong())
Toast.makeText(
context,
@@ -137,15 +143,34 @@ fun PlayerScreen(
modifier = Modifier.padding(16.dp),
)
- else -> AndroidView(
- factory = { ctx ->
- PlayerView(ctx).apply {
- player = exoPlayer
- useController = true
+ else -> {
+ AndroidView(
+ factory = { ctx ->
+ PlayerView(ctx).apply {
+ player = exoPlayer
+ useController = true
+ }
+ },
+ modifier = Modifier.fillMaxSize(),
+ )
+ // SponsorBlock segment count badge — small overlay top-left.
+ resolved?.let { r ->
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(12.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(Color(0xCC222222))
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ ) {
+ Text(
+ text = "SB: ${r.segments.size} segment${if (r.segments.size == 1) "" else "s"}",
+ color = Color.White,
+ style = MaterialTheme.typography.labelSmall,
+ )
}
- },
- modifier = Modifier.fillMaxSize(),
- )
+ }
+ }
}
}
}
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/net/RydClient.kt b/strawApp/src/main/kotlin/com/sulkta/straw/net/RydClient.kt
index 1cfa6a7e9..7c30dacd4 100644
--- a/strawApp/src/main/kotlin/com/sulkta/straw/net/RydClient.kt
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/net/RydClient.kt
@@ -8,6 +8,7 @@
package com.sulkta.straw.net
+import android.util.Log
import com.sulkta.straw.extractor.NewPipeDownloader
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -23,18 +24,29 @@ data class RydVotes(
)
object RydClient {
+ private const val TAG = "StrawRyd"
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
/** Blocking — call from Dispatchers.IO. */
fun fetch(videoId: String): RydVotes? {
+ val url = "https://returnyoutubedislikeapi.com/votes?videoId=$videoId"
+ Log.d(TAG, "fetch start: $videoId → $url")
val req = Request.Builder()
- .url("https://returnyoutubedislike.com/votes?videoId=$videoId")
+ .url(url)
.header("User-Agent", NewPipeDownloader.USER_AGENT)
+ .header("Accept", "application/json")
.build()
- return NewPipeDownloader.client().newCall(req).execute().use { r ->
- if (!r.isSuccessful) return@use null
- val body = r.body?.string() ?: return@use null
- runCatching { json.decodeFromString(body) }.getOrNull()
- }
+ return runCatching {
+ NewPipeDownloader.client().newCall(req).execute().use { r ->
+ val code = r.code
+ val bodyStr = r.body?.string() ?: ""
+ Log.d(TAG, "response: code=$code, body[0..120]=${bodyStr.take(120)}")
+ if (!r.isSuccessful) return@use null
+ runCatching { json.decodeFromString(bodyStr) }
+ .onFailure { Log.w(TAG, "json decode failed: ${it.message}") }
+ .getOrNull()
+ }
+ }.onFailure { Log.w(TAG, "fetch failed: ${it.javaClass.simpleName}: ${it.message}") }
+ .getOrNull()
}
}
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/net/SponsorBlockClient.kt b/strawApp/src/main/kotlin/com/sulkta/straw/net/SponsorBlockClient.kt
index 70d664ef0..c8b524905 100644
--- a/strawApp/src/main/kotlin/com/sulkta/straw/net/SponsorBlockClient.kt
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/net/SponsorBlockClient.kt
@@ -8,6 +8,7 @@
package com.sulkta.straw.net
+import android.util.Log
import com.sulkta.straw.extractor.NewPipeDownloader
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -32,13 +33,9 @@ data class SbSegment(
}
object SponsorBlockClient {
+ private const val TAG = "StrawSb"
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
- /**
- * Fetch SponsorBlock segments for [videoId] limited to [categories].
- * Returns segments matching the exact video ID (the API returns matches
- * for the prefix, so we filter client-side).
- */
fun fetch(
videoId: String,
categories: List = listOf("sponsor"),
@@ -46,18 +43,28 @@ object SponsorBlockClient {
val prefix = sha256Hex(videoId).substring(0, 4)
val urlStr = "https://sponsor.ajay.app/api/skipSegments/$prefix?" +
"categories=" + buildJsonArray(categories)
+ Log.d(TAG, "fetch: videoId=$videoId prefix=$prefix url=$urlStr")
val req = Request.Builder()
.url(urlStr)
.header("User-Agent", NewPipeDownloader.USER_AGENT)
+ .header("Accept", "application/json")
.build()
- return NewPipeDownloader.client().newCall(req).execute().use { r ->
- if (!r.isSuccessful) return@use emptyList()
- val body = r.body?.string() ?: return@use emptyList()
- val all = runCatching {
- json.decodeFromString>(body)
- }.getOrDefault(emptyList())
- all.firstOrNull { it.videoID == videoId }?.segments.orEmpty()
- }
+ return runCatching {
+ NewPipeDownloader.client().newCall(req).execute().use { r ->
+ val code = r.code
+ val bodyStr = r.body?.string() ?: ""
+ Log.d(TAG, "response: code=$code body_len=${bodyStr.length}")
+ if (!r.isSuccessful) return@use emptyList()
+ val all = runCatching {
+ json.decodeFromString>(bodyStr)
+ }.onFailure { Log.w(TAG, "json decode failed: ${it.message}") }
+ .getOrDefault(emptyList())
+ val mine = all.firstOrNull { it.videoID == videoId }?.segments.orEmpty()
+ Log.d(TAG, "armed ${mine.size} segments for $videoId (response had ${all.size} matching-prefix videos)")
+ mine
+ }
+ }.onFailure { Log.w(TAG, "fetch failed: ${it.javaClass.simpleName}: ${it.message}") }
+ .getOrDefault(emptyList())
}
private fun buildJsonArray(items: List): String =
diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/util/HtmlText.kt b/strawApp/src/main/kotlin/com/sulkta/straw/util/HtmlText.kt
new file mode 100644
index 000000000..f228b23d2
--- /dev/null
+++ b/strawApp/src/main/kotlin/com/sulkta/straw/util/HtmlText.kt
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Sulkta-Coop
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Strip HTML tags from NewPipeExtractor's description.content for plain-text
+ * rendering. Day-3 polish replaces this with a real Markwon/Compose annotated
+ * renderer; for now we just want readable text.
+ */
+
+package com.sulkta.straw.util
+
+private val tagRe = Regex("?[^>]+>")
+private val entityRe = Regex("&[a-zA-Z#0-9]+;")
+private val brRe = Regex("
", RegexOption.IGNORE_CASE)
+private val pCloseRe = Regex("
", RegexOption.IGNORE_CASE)
+
+private val entityMap = mapOf(
+ "&" to "&", "<" to "<", ">" to ">", """ to "\"",
+ "'" to "'", " " to " ", "'" to "'", """ to "\"",
+)
+
+fun stripHtml(input: String): String {
+ if (input.isBlank()) return ""
+ var s = input
+ s = brRe.replace(s, "\n")
+ s = pCloseRe.replace(s, "\n\n")
+ s = tagRe.replace(s, "")
+ s = entityRe.replace(s) { entityMap[it.value] ?: it.value }
+ return s.trim().replace(Regex("\n{3,}"), "\n\n")
+}
diff --git a/strawApp/src/main/res/raw/sectigo_dv_r36.crt b/strawApp/src/main/res/raw/sectigo_dv_r36.crt
new file mode 100644
index 000000000..af4a9d661
--- /dev/null
+++ b/strawApp/src/main/res/raw/sectigo_dv_r36.crt
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
+MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
+Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
+HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
+MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
+YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
+AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
+RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
+12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
+EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
+M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
+38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
+pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
+2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
+xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
+lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
+DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
+KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
+VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
+UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
+AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
+Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
+BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
+YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
+ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
+q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
+h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
+5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
+4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
+unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
+WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
+7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
+r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
+JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
+-----END CERTIFICATE-----
diff --git a/strawApp/src/main/res/xml/network_security_config.xml b/strawApp/src/main/res/xml/network_security_config.xml
new file mode 100644
index 000000000..4ba64bd35
--- /dev/null
+++ b/strawApp/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+ returnyoutubedislike.com
+ returnyoutubedislikeapi.com
+
+
+
+
+
+