Skip to content

Commit

Permalink
Handle YouTube /live URLs
Browse files Browse the repository at this point in the history
Fixes #31
  • Loading branch information
arkon committed May 19, 2024
1 parent 6d16db1 commit df4dfed
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 49 deletions.
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.livetl.android.data.chat
import android.annotation.SuppressLint

import android.content.Context
import android.webkit.JavascriptInterface
import android.webkit.WebView
Expand Down Expand Up @@ -36,9 +36,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

@SuppressLint("SetJavaScriptEnabled")
class ChatService @Inject constructor(
@ApplicationContext context: Context,
private val json: Json,
Expand Down Expand Up @@ -98,7 +96,6 @@ class ChatService @Inject constructor(
clearMessages()
}

@ExperimentalTime
@Suppress("Unused")
@JavascriptInterface
fun receiveMessages(data: String) {
Expand Down Expand Up @@ -162,7 +159,6 @@ class ChatService @Inject constructor(
/**
* Calculates number of microseconds from [now] until [microseconds].
*/
@ExperimentalTime
private fun getMicrosecondDiff(now: Long, microseconds: Long): Duration {
val diff = now - microseconds
Timber.d(
Expand Down
1 change: 0 additions & 1 deletion app/src/main/kotlin/com/livetl/android/data/chat/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ sealed interface ChatMessage {
}
}

// TODO: handle member milestone messages
data class NewMember(override val author: MessageAuthor, override val timestamp: Long) : ChatMessage {
override val content: List<ChatMessageContent> = emptyList()

Expand Down
51 changes: 25 additions & 26 deletions app/src/main/kotlin/com/livetl/android/data/feed/FeedService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,24 @@ import javax.inject.Inject
class FeedService @Inject constructor(private val client: HttpClient, private val json: Json) {
suspend fun getFeed(organization: String? = "Hololive", status: StreamStatus): List<Stream> =
withContext(SupervisorJob() + Dispatchers.IO) {
val result =
client.get {
url {
baseUrl()
path("api", "v2", "videos")
parameter("status", status.apiValue)
parameter("lang", "all")
parameter("type", "stream")
parameter("include", "description,live_info")
parameter("org", organization)
parameter("sort", status.sortField)
parameter("order", if (status.sortAscending) "asc" else "desc")
parameter("limit", "50")
parameter("offset", "0")
parameter("paginated", "<empty>")
parameter("max_upcoming_hours", "48")
}
baseHeaders()
val result = client.get {
url {
baseUrl()
path("api", "v2", "videos")
parameter("status", status.apiValue)
parameter("lang", "all")
parameter("type", "stream")
parameter("include", "description,live_info")
parameter("org", organization)
parameter("sort", status.sortField)
parameter("order", if (status.sortAscending) "asc" else "desc")
parameter("limit", "50")
parameter("offset", "0")
parameter("paginated", "<empty>")
parameter("max_upcoming_hours", "48")
}
baseHeaders()
}

try {
val response: HolodexVideosResponse = json.decodeFromString(result.bodyAsText())
Expand All @@ -49,16 +48,16 @@ class FeedService @Inject constructor(private val client: HttpClient, private va
}

suspend fun getVideoInfo(videoId: String): Stream = withContext(SupervisorJob() + Dispatchers.IO) {
val result =
client.get {
url {
baseUrl()
path("api", "v2", "videos", videoId)
}
baseHeaders()
val result = client.get {
url {
baseUrl()
path("api", "v2", "videos", videoId)
}
baseHeaders()
}

json.decodeFromString(result.bodyAsText())
val body = result.bodyAsText()
json.decodeFromString(body)
}

private fun URLBuilder.baseUrl() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class VideoIdParser @Inject constructor() {
return matcher.group(3)
}

matcher = LIVE_LINK_PATTERN.matcher(url)
if (matcher.find()) {
return matcher.group(3)
}

matcher = SHORT_LINK_PATTERN.matcher(url)
if (matcher.find()) {
return matcher.group(3)
Expand All @@ -44,6 +49,10 @@ private val PAGE_LINK_PATTERN by lazy {
"(http|https)://(www\\.|m.|)youtube\\.com/watch\\?v=(.+?)( |\\z|&)".toPattern()
}

private val LIVE_LINK_PATTERN by lazy {
"(http|https)://(www\\.|m.|)youtube\\.com/live/(.+?)( |\\z|&|\\?)".toPattern()
}

private val SHORT_LINK_PATTERN by lazy {
"(http|https)://(www\\.|)youtu.be/(.+?)( |\\z|&)".toPattern()
}
Expand Down
35 changes: 19 additions & 16 deletions app/src/main/kotlin/com/livetl/android/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package com.livetl.android.ui

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import com.livetl.android.ui.navigation.Route
import com.livetl.android.ui.navigation.mainNavHost
import com.livetl.android.ui.navigation.navigateToPlayer
import com.livetl.android.ui.theme.LiveTLTheme
import com.livetl.android.util.AppPreferences
import com.livetl.android.util.waitUntil
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -28,25 +29,20 @@ class MainActivity : ComponentActivity() {

enableEdgeToEdge()

val startRoute =
when (prefs.showWelcomeScreen().get()) {
true -> Route.Welcome
false -> Route.Home
}
val startRoute = when (prefs.showWelcomeScreen().get()) {
true -> Route.Welcome
false -> Route.Home
}

setContent {
LiveTLTheme {
navController =
mainNavHost(
startRoute = startRoute,
)
navController = mainNavHost(
startRoute = startRoute,
)
}
}

// Needs to be delayed so the app contents can load first
Handler(Looper.getMainLooper()).post {
onNewIntent(intent)
}
onNewIntent(intent)
}

override fun onNewIntent(intent: Intent) {
Expand All @@ -64,6 +60,13 @@ class MainActivity : ComponentActivity() {
}

private fun handleVideoIntent(data: String?) {
data?.let { navController?.navigateToPlayer(it) }
data?.let {
lifecycleScope.launch {
// The app might take some time to actually load up
waitUntil({ navController != null }) {
navController?.navigateToPlayer(it)
}
}
}
}
}
21 changes: 21 additions & 0 deletions app/src/main/kotlin/com/livetl/android/util/CoroutineUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,32 @@ package com.livetl.android.util
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@DelicateCoroutinesApi
fun runOnMainThread(block: () -> Unit) {
GlobalScope.launch(Dispatchers.Main) {
block()
}
}

suspend fun waitUntil(
predicate: () -> Boolean,
timeout: Duration = 10.seconds,
delayTime: Duration = 1.seconds,
block: () -> Unit,
) {
withTimeout(timeout) {
while (true) {
delay(delayTime)
if (predicate()) {
block()
return@withTimeout
}
}
}
}

0 comments on commit df4dfed

Please sign in to comment.