mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Play theme songs for series
This commit is contained in:
parent
ba92001412
commit
1816317c68
5 changed files with 102 additions and 17 deletions
|
|
@ -25,6 +25,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
|
import com.github.damontecres.dolphin.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.dolphin.preferences.AppPreferences
|
import com.github.damontecres.dolphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.dolphin.preferences.DefaultUserConfiguration
|
import com.github.damontecres.dolphin.preferences.DefaultUserConfiguration
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
|
|
@ -48,6 +49,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
|
lateinit var userPreferencesDataStore: DataStore<AppPreferences>
|
||||||
|
|
||||||
|
@AuthOkHttpClient
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
|
|
@ -64,7 +66,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
) {
|
) {
|
||||||
CoilConfig(serverRepository, okHttpClient, false)
|
CoilConfig(okHttpClient, false)
|
||||||
|
|
||||||
var isRestoringSession by remember { mutableStateOf(true) }
|
var isRestoringSession by remember { mutableStateOf(true) }
|
||||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import com.github.damontecres.dolphin.BuildConfig
|
import com.github.damontecres.dolphin.BuildConfig
|
||||||
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -15,11 +16,21 @@ import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||||
import org.jellyfin.sdk.createJellyfin
|
import org.jellyfin.sdk.createJellyfin
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import org.jellyfin.sdk.model.DeviceInfo
|
import org.jellyfin.sdk.model.DeviceInfo
|
||||||
|
import javax.inject.Qualifier
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class AuthOkHttpClient
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class StandardOkHttpClient
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object AppModule {
|
object AppModule {
|
||||||
|
@StandardOkHttpClient
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun okHttpClient() =
|
fun okHttpClient() =
|
||||||
|
|
@ -29,9 +40,33 @@ object AppModule {
|
||||||
// TODO user agent, timeouts, logging, etc
|
// TODO user agent, timeouts, logging, etc
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
@AuthOkHttpClient
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun okHttpFactory(okHttpClient: OkHttpClient) = OkHttpFactory(okHttpClient)
|
fun authOkHttpClient(
|
||||||
|
serverRepository: ServerRepository,
|
||||||
|
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
||||||
|
) = okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.addInterceptor {
|
||||||
|
val request = it.request()
|
||||||
|
val newRequest =
|
||||||
|
serverRepository.currentUser?.accessToken?.let { token ->
|
||||||
|
request
|
||||||
|
.newBuilder()
|
||||||
|
.addHeader(
|
||||||
|
"Authorization",
|
||||||
|
"MediaBrowser Token=\"$token\"",
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
it.proceed(newRequest ?: request)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun okHttpFactory(
|
||||||
|
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
||||||
|
) = OkHttpFactory(okHttpClient)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,12 @@ import coil3.network.cachecontrol.CacheControlCacheStrategy
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.util.DebugLogger
|
import coil3.util.DebugLogger
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class)
|
@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CoilConfig(
|
fun CoilConfig(
|
||||||
serverRepository: ServerRepository,
|
|
||||||
okHttpClient: OkHttpClient,
|
okHttpClient: OkHttpClient,
|
||||||
debugLogging: Boolean,
|
debugLogging: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
@ -37,18 +34,7 @@ fun CoilConfig(
|
||||||
add(
|
add(
|
||||||
OkHttpNetworkFetcherFactory(
|
OkHttpNetworkFetcherFactory(
|
||||||
cacheStrategy = { CacheControlCacheStrategy() },
|
cacheStrategy = { CacheControlCacheStrategy() },
|
||||||
callFactory = {
|
callFactory = { okHttpClient },
|
||||||
Call.Factory { request ->
|
|
||||||
// Ref: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f
|
|
||||||
val token = serverRepository.currentUser?.accessToken
|
|
||||||
okHttpClient.newCall(
|
|
||||||
request
|
|
||||||
.newBuilder()
|
|
||||||
.addHeader("Authorization", "MediaBrowser Token=\"$token\"")
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.github.damontecres.dolphin.ui.detail
|
package com.github.damontecres.dolphin.ui.detail
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -10,9 +12,16 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.data.model.Video
|
import com.github.damontecres.dolphin.data.model.Video
|
||||||
|
import com.github.damontecres.dolphin.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||||
|
|
@ -26,12 +35,16 @@ import com.github.damontecres.dolphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
|
import com.github.damontecres.dolphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.dolphin.util.LoadingState
|
import com.github.damontecres.dolphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
|
@ -48,7 +61,10 @@ class SeriesViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
@param:ApplicationContext val context: Context,
|
||||||
|
@param:AuthOkHttpClient private val okHttpClient: OkHttpClient,
|
||||||
) : ItemViewModel<Video>(api) {
|
) : ItemViewModel<Video>(api) {
|
||||||
|
private var player: Player? = null
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
@ -80,6 +96,7 @@ class SeriesViewModel
|
||||||
episodes.value = episodePager.orEmpty()
|
episodes.value = episodePager.orEmpty()
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
|
maybePlayThemeSong()
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
seasons.value = listOf()
|
seasons.value = listOf()
|
||||||
|
|
@ -90,6 +107,49 @@ class SeriesViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun maybePlayThemeSong() {
|
||||||
|
// TODO user preference to enable/disable this
|
||||||
|
viewModelScope.launch(ExceptionHandler()) {
|
||||||
|
val themeSongs = api.libraryApi.getThemeSongs(seriesId).content
|
||||||
|
themeSongs.items.firstOrNull()?.let { theme ->
|
||||||
|
theme.mediaSources?.firstOrNull()?.let { source ->
|
||||||
|
val url =
|
||||||
|
api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||||
|
theme.id,
|
||||||
|
container = listOf("opus", "mp3", "aaa", "flac"),
|
||||||
|
)
|
||||||
|
Timber.v("Found theme song for series $seriesId")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val player =
|
||||||
|
ExoPlayer
|
||||||
|
.Builder(context)
|
||||||
|
.setMediaSourceFactory(
|
||||||
|
DefaultMediaSourceFactory(
|
||||||
|
OkHttpDataSource.Factory(okHttpClient),
|
||||||
|
),
|
||||||
|
).build()
|
||||||
|
.apply {
|
||||||
|
volume = .1f
|
||||||
|
playWhenReady = true
|
||||||
|
this@SeriesViewModel.player = this
|
||||||
|
}
|
||||||
|
addCloseable {
|
||||||
|
player.release()
|
||||||
|
}
|
||||||
|
player.setMediaItem(MediaItem.fromUri(url))
|
||||||
|
player.prepare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
player?.release()
|
||||||
|
player = null
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getSeasons(item: BaseItem): ApiRequestPager<GetItemsRequest>? {
|
private suspend fun getSeasons(item: BaseItem): ApiRequestPager<GetItemsRequest>? {
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ fun SeriesOverview(
|
||||||
position = it
|
position = it
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
|
viewModel.release()
|
||||||
val resumePosition =
|
val resumePosition =
|
||||||
it.data.userData
|
it.data.userData
|
||||||
?.playbackPositionTicks
|
?.playbackPositionTicks
|
||||||
|
|
@ -143,6 +144,7 @@ fun SeriesOverview(
|
||||||
},
|
},
|
||||||
playOnClick = { resume ->
|
playOnClick = { resume ->
|
||||||
episodes.getOrNull(position.episodeRowIndex)?.let {
|
episodes.getOrNull(position.episodeRowIndex)?.let {
|
||||||
|
viewModel.release()
|
||||||
navigationManager.navigateTo(
|
navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
it.id,
|
it.id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue