mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/customize-home
This commit is contained in:
commit
d99c947068
20 changed files with 290 additions and 132 deletions
|
|
@ -31,7 +31,7 @@ This is not a fork of the [official client](https://github.com/jellyfin/jellyfin
|
||||||
### User interface
|
### User interface
|
||||||
|
|
||||||
- A navigation drawer for quick access to libraries, favorites, search, and settings from almost anywhere in the app
|
- A navigation drawer for quick access to libraries, favorites, search, and settings from almost anywhere in the app
|
||||||
- Integration with [Seerr](https://github.com/seerr-team/seerr) to discover new movies and TV shows
|
- Integration with [Jellyseerr](https://github.com/seerr-team/seerr) to discover new movies and TV shows
|
||||||
- Option to combine Continue Watching & Next Up rows
|
- Option to combine Continue Watching & Next Up rows
|
||||||
- Show Movie/TV Show titles when browsing libraries
|
- Show Movie/TV Show titles when browsing libraries
|
||||||
- Play theme music, if available
|
- Play theme music, if available
|
||||||
|
|
@ -93,6 +93,8 @@ Requires Android 6+ (or Fire TV OS 6+) and Jellyfin server `10.10.x` or `10.11.x
|
||||||
|
|
||||||
The app is tested on a variety of Android TV/Fire TV OS devices, but if you encounter issues, please file an issue!
|
The app is tested on a variety of Android TV/Fire TV OS devices, but if you encounter issues, please file an issue!
|
||||||
|
|
||||||
|
Jellyseerr integration is tested with `v2.7.3`. Older versions may not work.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Issues and pull requests are always welcome! Please check before submitting that your issue or pull request is not a duplicate.
|
Issues and pull requests are always welcome! Please check before submitting that your issue or pull request is not a duplicate.
|
||||||
|
|
|
||||||
|
|
@ -419,8 +419,12 @@ class MainActivityViewModel
|
||||||
prefs.currentUserId?.toUUIDOrNull(),
|
prefs.currentUserId?.toUUIDOrNull(),
|
||||||
)
|
)
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
// Restored
|
if (current.user.hasPin) {
|
||||||
navigationManager.navigateTo(SetupDestination.AppContent(current))
|
navigationManager.navigateTo(SetupDestination.UserList(current.server))
|
||||||
|
} else {
|
||||||
|
// Restored
|
||||||
|
navigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Did not restore
|
// Did not restore
|
||||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||||
|
|
|
||||||
|
|
@ -789,7 +789,7 @@ sealed interface AppPreference<Pref, T> {
|
||||||
val MpvGpuNext =
|
val MpvGpuNext =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
title = R.string.mpv_use_gpu_next,
|
title = R.string.mpv_use_gpu_next,
|
||||||
defaultValue = true,
|
defaultValue = false,
|
||||||
getter = { it.playbackPreferences.mpvOptions.useGpuNext },
|
getter = { it.playbackPreferences.mpvOptions.useGpuNext },
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
prefs.updateMpvOptions { useGpuNext = value }
|
prefs.updateMpvOptions { useGpuNext = value }
|
||||||
|
|
|
||||||
|
|
@ -209,4 +209,12 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG)
|
showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updateMpvOptions {
|
||||||
|
useGpuNext = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import okhttp3.OkHttpClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages saves/loading Seerr servers from the local DB. Also will update the current [SeerrApi] as needed.
|
* Manages saves/loading Seerr servers from the local DB. Also will update the current [SeerrApi] as needed.
|
||||||
|
|
@ -137,7 +138,17 @@ class SeerrServerRepository
|
||||||
username: String?,
|
username: String?,
|
||||||
passwordOrApiKey: String,
|
passwordOrApiKey: String,
|
||||||
): LoadingState {
|
): LoadingState {
|
||||||
val api = SeerrApiClient(url, passwordOrApiKey, okHttpClient)
|
val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY }
|
||||||
|
val api =
|
||||||
|
SeerrApiClient(
|
||||||
|
url,
|
||||||
|
apiKey,
|
||||||
|
okHttpClient
|
||||||
|
.newBuilder()
|
||||||
|
.connectTimeout(5.seconds)
|
||||||
|
.readTimeout(6.seconds)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
login(api, authMethod, username, passwordOrApiKey)
|
login(api, authMethod, username, passwordOrApiKey)
|
||||||
return LoadingState.Success
|
return LoadingState.Success
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ fun DiscoverMovieDetails(
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
val trailers by viewModel.trailers.observeAsState(listOf())
|
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
val recommended by viewModel.similar.observeAsState(listOf())
|
val recommended by viewModel.recommended.observeAsState(listOf())
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||||
val userConfig by viewModel.userConfig.collectAsState(null)
|
val userConfig by viewModel.userConfig.collectAsState(null)
|
||||||
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
val request4kEnabled by viewModel.request4kEnabled.collectAsState(false)
|
||||||
|
|
@ -365,7 +365,7 @@ fun DiscoverMovieDetailsContent(
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.recommended),
|
title = stringResource(R.string.recommended),
|
||||||
items = similar,
|
items = recommended,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
position = RECOMMENDED_ROW
|
position = RECOMMENDED_ROW
|
||||||
onClickItem.invoke(index, item)
|
onClickItem.invoke(index, item)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = DiscoverMovieViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = DiscoverMovieViewModel.Factory::class)
|
||||||
class DiscoverMovieViewModel
|
class DiscoverMovieViewModel
|
||||||
|
|
@ -67,8 +68,8 @@ class DiscoverMovieViewModel
|
||||||
|
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
val people = MutableLiveData<List<DiscoverItem>>(listOf())
|
val people = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
val similar = MutableLiveData<List<DiscoverItem>>(listOf())
|
val similar = MutableLiveData<List<DiscoverItem>>()
|
||||||
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
val recommended = MutableLiveData<List<DiscoverItem>>()
|
||||||
val canCancelRequest = MutableStateFlow(false)
|
val canCancelRequest = MutableStateFlow(false)
|
||||||
|
|
||||||
val userConfig = seerrServerRepository.current.map { it?.config }
|
val userConfig = seerrServerRepository.current.map { it?.config }
|
||||||
|
|
@ -99,6 +100,7 @@ class DiscoverMovieViewModel
|
||||||
"Error fetching movie",
|
"Error fetching movie",
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
Timber.v("Init for movie %s", item.id)
|
||||||
val movie = fetchAndSetItem().await()
|
val movie = fetchAndSetItem().await()
|
||||||
val discoveredItem = DiscoverItem(movie)
|
val discoveredItem = DiscoverItem(movie)
|
||||||
backdropService.submit(discoveredItem)
|
backdropService.submit(discoveredItem)
|
||||||
|
|
@ -116,7 +118,7 @@ class DiscoverMovieViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result =
|
val result =
|
||||||
seerrService.api.moviesApi
|
seerrService.api.moviesApi
|
||||||
.movieMovieIdSimilarGet(movieId = item.id, page = 2)
|
.movieMovieIdSimilarGet(movieId = item.id, page = 1)
|
||||||
.results
|
.results
|
||||||
?.map(::DiscoverItem)
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
@ -125,11 +127,11 @@ class DiscoverMovieViewModel
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result =
|
val result =
|
||||||
seerrService.api.moviesApi
|
seerrService.api.moviesApi
|
||||||
.movieMovieIdRecommendationsGet(movieId = item.id, page = 2)
|
.movieMovieIdRecommendationsGet(movieId = item.id, page = 1)
|
||||||
.results
|
.results
|
||||||
?.map(::DiscoverItem)
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
similar.setValueOnMain(result)
|
recommended.setValueOnMain(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val people =
|
val people =
|
||||||
|
|
@ -147,7 +149,7 @@ class DiscoverMovieViewModel
|
||||||
?.filter { it.type == RelatedVideo.Type.TRAILER }
|
?.filter { it.type == RelatedVideo.Type.TRAILER }
|
||||||
?.filter { it.name.isNotNullOrBlank() && it.url.isNotNullOrBlank() }
|
?.filter { it.name.isNotNullOrBlank() && it.url.isNotNullOrBlank() }
|
||||||
?.map {
|
?.map {
|
||||||
RemoteTrailer(it.name!!, it.url!!, null)
|
RemoteTrailer(it.name!!, it.url!!, it.site)
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
this@DiscoverMovieViewModel.trailers.setValueOnMain(trailers)
|
this@DiscoverMovieViewModel.trailers.setValueOnMain(trailers)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ fun DiscoverSeriesDetails(
|
||||||
val item by viewModel.tvSeries.observeAsState()
|
val item by viewModel.tvSeries.observeAsState()
|
||||||
val seasons by viewModel.seasons.observeAsState(listOf())
|
val seasons by viewModel.seasons.observeAsState(listOf())
|
||||||
val people by viewModel.people.observeAsState(listOf())
|
val people by viewModel.people.observeAsState(listOf())
|
||||||
|
val trailers by viewModel.trailers.observeAsState(listOf())
|
||||||
val similar by viewModel.similar.observeAsState(listOf())
|
val similar by viewModel.similar.observeAsState(listOf())
|
||||||
val recommended by viewModel.recommended.observeAsState(listOf())
|
val recommended by viewModel.recommended.observeAsState(listOf())
|
||||||
val userConfig by viewModel.userConfig.collectAsState(null)
|
val userConfig by viewModel.userConfig.collectAsState(null)
|
||||||
|
|
@ -155,7 +156,7 @@ fun DiscoverSeriesDetails(
|
||||||
trailerOnClick = {
|
trailerOnClick = {
|
||||||
TrailerService.onClick(context, it, viewModel::navigateTo)
|
TrailerService.onClick(context, it, viewModel::navigateTo)
|
||||||
},
|
},
|
||||||
trailers = listOf(),
|
trailers = trailers,
|
||||||
requestOnClick = {
|
requestOnClick = {
|
||||||
item.id?.let { id ->
|
item.id?.let { id ->
|
||||||
if (request4kEnabled) {
|
if (request4kEnabled) {
|
||||||
|
|
@ -255,7 +256,7 @@ fun DiscoverSeriesDetailsContent(
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
|
||||||
var position by rememberInt()
|
var position by rememberInt()
|
||||||
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
val focusRequesters = remember { List(RECOMMENDED_ROW + 1) { FocusRequester() } }
|
||||||
val playFocusRequester = remember { FocusRequester() }
|
val playFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
|
|
@ -398,7 +399,7 @@ fun DiscoverSeriesDetailsContent(
|
||||||
item {
|
item {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(R.string.recommended),
|
title = stringResource(R.string.recommended),
|
||||||
items = similar,
|
items = recommended,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
position = RECOMMENDED_ROW
|
position = RECOMMENDED_ROW
|
||||||
onClickItem.invoke(index, item)
|
onClickItem.invoke(index, item)
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,20 @@ import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest
|
import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.Season
|
import com.github.damontecres.wholphin.api.seerr.model.Season
|
||||||
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
import com.github.damontecres.wholphin.api.seerr.model.TvDetails
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
|
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
|
@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = DiscoverSeriesViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = DiscoverSeriesViewModel.Factory::class)
|
||||||
class DiscoverSeriesViewModel
|
class DiscoverSeriesViewModel
|
||||||
|
|
@ -62,8 +66,8 @@ class DiscoverSeriesViewModel
|
||||||
val seasons = MutableLiveData<List<Season>>(listOf())
|
val seasons = MutableLiveData<List<Season>>(listOf())
|
||||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||||
val people = MutableLiveData<List<DiscoverItem>>(listOf())
|
val people = MutableLiveData<List<DiscoverItem>>(listOf())
|
||||||
val similar = MutableLiveData<List<DiscoverItem>>(listOf())
|
val similar = MutableLiveData<List<DiscoverItem>>()
|
||||||
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
|
val recommended = MutableLiveData<List<DiscoverItem>>()
|
||||||
val canCancelRequest = MutableStateFlow(false)
|
val canCancelRequest = MutableStateFlow(false)
|
||||||
|
|
||||||
val userConfig = seerrServerRepository.current.map { it?.config }
|
val userConfig = seerrServerRepository.current.map { it?.config }
|
||||||
|
|
@ -94,6 +98,7 @@ class DiscoverSeriesViewModel
|
||||||
"Error fetching movie",
|
"Error fetching movie",
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
Timber.v("Init for tv %s", item.id)
|
||||||
val tv = fetchAndSetItem().await()
|
val tv = fetchAndSetItem().await()
|
||||||
val discoveredItem = DiscoverItem(tv)
|
val discoveredItem = DiscoverItem(tv)
|
||||||
backdropService.submit(discoveredItem)
|
backdropService.submit(discoveredItem)
|
||||||
|
|
@ -110,8 +115,8 @@ class DiscoverSeriesViewModel
|
||||||
if (!similar.isInitialized) {
|
if (!similar.isInitialized) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result =
|
val result =
|
||||||
seerrService.api.moviesApi
|
seerrService.api.tvApi
|
||||||
.movieMovieIdSimilarGet(movieId = item.id, page = 2)
|
.tvTvIdSimilarGet(tvId = item.id, page = 1)
|
||||||
.results
|
.results
|
||||||
?.map(::DiscoverItem)
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
@ -119,12 +124,12 @@ class DiscoverSeriesViewModel
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result =
|
val result =
|
||||||
seerrService.api.moviesApi
|
seerrService.api.tvApi
|
||||||
.movieMovieIdRecommendationsGet(movieId = item.id, page = 2)
|
.tvTvIdRecommendationsGet(tvId = item.id, page = 1)
|
||||||
.results
|
.results
|
||||||
?.map(::DiscoverItem)
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
similar.setValueOnMain(result)
|
recommended.setValueOnMain(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val people =
|
val people =
|
||||||
|
|
@ -137,6 +142,15 @@ class DiscoverSeriesViewModel
|
||||||
?.map(::DiscoverItem)
|
?.map(::DiscoverItem)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
this@DiscoverSeriesViewModel.people.setValueOnMain(people)
|
this@DiscoverSeriesViewModel.people.setValueOnMain(people)
|
||||||
|
|
||||||
|
val trailers =
|
||||||
|
tv.relatedVideos
|
||||||
|
?.filter { it.type == RelatedVideo.Type.TRAILER }
|
||||||
|
?.filter { it.name.isNotNullOrBlank() && it.url.isNotNullOrBlank() }
|
||||||
|
?.map {
|
||||||
|
RemoteTrailer(it.name!!, it.url!!, it.site)
|
||||||
|
}.orEmpty()
|
||||||
|
this@DiscoverSeriesViewModel.trailers.setValueOnMain(trailers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateTo(destination: Destination) {
|
fun navigateTo(destination: Destination) {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
@ -112,6 +113,9 @@ class SeerrRequestsViewModel
|
||||||
|
|
||||||
state.update { it.copy(requests = DataLoadingState.Success(results)) }
|
state.update { it.copy(requests = DataLoadingState.Success(results)) }
|
||||||
}
|
}
|
||||||
|
}.catch { ex ->
|
||||||
|
Timber.e(ex, "Error fetching requests")
|
||||||
|
state.update { it.copy(requests = DataLoadingState.Error(ex)) }
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
|
||||||
|
|
@ -206,15 +207,15 @@ fun HomePageContent(
|
||||||
var firstFocused by remember { mutableStateOf(false) }
|
var firstFocused by remember { mutableStateOf(false) }
|
||||||
if (takeFocus) {
|
if (takeFocus) {
|
||||||
LaunchedEffect(homeRows) {
|
LaunchedEffect(homeRows) {
|
||||||
if (!firstFocused) {
|
if (!firstFocused && homeRows.isNotEmpty()) {
|
||||||
if (position.row >= 0) {
|
if (position.row >= 0) {
|
||||||
rowFocusRequesters[position.row].tryRequestFocus()
|
val index = position.row.coerceIn(0, rowFocusRequesters.lastIndex)
|
||||||
|
rowFocusRequesters.getOrNull(index)?.tryRequestFocus()
|
||||||
firstFocused = true
|
firstFocused = true
|
||||||
} else {
|
} else {
|
||||||
// Waiting for the first home row to load, then focus on it
|
// Waiting for the first home row to load, then focus on it
|
||||||
homeRows
|
homeRows
|
||||||
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
.indexOfFirstOrNull { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
||||||
.takeIf { it >= 0 }
|
|
||||||
?.let {
|
?.let {
|
||||||
rowFocusRequesters[it].tryRequestFocus()
|
rowFocusRequesters[it].tryRequestFocus()
|
||||||
firstFocused = true
|
firstFocused = true
|
||||||
|
|
|
||||||
|
|
@ -219,18 +219,22 @@ class PlaybackViewModel
|
||||||
PlayerBackend.PREFER_MPV -> if (isHdr) PlayerBackend.EXO_PLAYER else PlayerBackend.MPV
|
PlayerBackend.PREFER_MPV -> if (isHdr) PlayerBackend.EXO_PLAYER else PlayerBackend.MPV
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.i("Selected backend: %s", playerBackend)
|
Timber.d("Selected backend: %s", playerBackend)
|
||||||
withContext(Dispatchers.Main) {
|
if (currentPlayer.value?.backend != playerBackend) {
|
||||||
disconnectPlayer()
|
Timber.i("Switching player backend to %s", playerBackend)
|
||||||
}
|
withContext(Dispatchers.Main) {
|
||||||
|
disconnectPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
player =
|
player =
|
||||||
playerFactory.createVideoPlayer(
|
playerFactory.createVideoPlayer(
|
||||||
playerBackend,
|
playerBackend,
|
||||||
preferences.appPreferences.playbackPreferences,
|
preferences.appPreferences.playbackPreferences,
|
||||||
)
|
)
|
||||||
currentPlayer.update {
|
currentPlayer.update {
|
||||||
PlayerState(player, playerBackend)
|
PlayerState(player, playerBackend)
|
||||||
|
}
|
||||||
|
configurePlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,7 +433,6 @@ class PlaybackViewModel
|
||||||
|
|
||||||
// Create the correct player for the media
|
// Create the correct player for the media
|
||||||
createPlayer(videoStream?.hdr == true)
|
createPlayer(videoStream?.hdr == true)
|
||||||
configurePlayer()
|
|
||||||
|
|
||||||
val subtitleStreams =
|
val subtitleStreams =
|
||||||
mediaSource.mediaStreams
|
mediaSource.mediaStreams
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences
|
import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences
|
||||||
|
|
@ -504,13 +503,7 @@ fun PreferencesContent(
|
||||||
AddSeerServerDialog(
|
AddSeerServerDialog(
|
||||||
currentUsername = currentUser?.name,
|
currentUsername = currentUser?.name,
|
||||||
status = status,
|
status = status,
|
||||||
onSubmit = { url: String, username: String?, passwordOrApiKey: String, method: SeerrAuthMethod ->
|
onSubmit = seerrVm::submitServer,
|
||||||
if (method == SeerrAuthMethod.API_KEY) {
|
|
||||||
seerrVm.submitServer(url, passwordOrApiKey)
|
|
||||||
} else {
|
|
||||||
seerrVm.submitServer(url, username ?: "", passwordOrApiKey, method)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
|
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -30,6 +32,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
|
import com.github.damontecres.wholphin.ui.components.Button
|
||||||
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
import com.github.damontecres.wholphin.ui.components.EditTextBox
|
||||||
import com.github.damontecres.wholphin.ui.components.TextButton
|
import com.github.damontecres.wholphin.ui.components.TextButton
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
@ -240,7 +243,7 @@ fun AddSeerrServerUsername(
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
isInputValid = { true },
|
isInputValid = { true },
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(usernameFocusRequester),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -283,12 +286,23 @@ fun AddSeerrServerUsername(
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextButton(
|
Button(
|
||||||
stringRes = R.string.submit,
|
|
||||||
onClick = { onSubmit.invoke(url, username, password) },
|
onClick = { onSubmit.invoke(url, username, password) },
|
||||||
enabled = error.isNullOrBlank() && url.isNotNullOrBlank() && username.isNotNullOrBlank() && password.isNotNullOrBlank(),
|
enabled =
|
||||||
|
error.isNullOrBlank() && url.isNotNullOrBlank() && username.isNotNullOrBlank() &&
|
||||||
|
status != LoadingState.Loading,
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
)
|
) {
|
||||||
|
if (status != LoadingState.Loading) {
|
||||||
|
Text(text = stringResource(R.string.submit))
|
||||||
|
} else {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.border,
|
||||||
|
modifier =
|
||||||
|
Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
fun AddSeerServerDialog(
|
fun AddSeerServerDialog(
|
||||||
currentUsername: String?,
|
currentUsername: String?,
|
||||||
status: LoadingState,
|
status: LoadingState,
|
||||||
onSubmit: (url: String, username: String?, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var authMethod by remember { mutableStateOf<SeerrAuthMethod?>(null) }
|
var authMethod by remember { mutableStateOf<SeerrAuthMethod?>(null) }
|
||||||
|
|
@ -49,7 +49,7 @@ fun AddSeerServerDialog(
|
||||||
) {
|
) {
|
||||||
AddSeerrServerApiKey(
|
AddSeerrServerApiKey(
|
||||||
onSubmit = { url, apiKey ->
|
onSubmit = { url, apiKey ->
|
||||||
onSubmit.invoke(url, null, apiKey, SeerrAuthMethod.API_KEY)
|
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
|
||||||
},
|
},
|
||||||
status = status,
|
status = status,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.setup.seerr
|
package com.github.damontecres.wholphin.ui.setup.seerr
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
|
import com.github.damontecres.wholphin.api.seerr.infrastructure.ClientException
|
||||||
|
|
@ -8,18 +9,22 @@ import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SeerrService
|
import com.github.damontecres.wholphin.services.SeerrService
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SwitchSeerrViewModel
|
class SwitchSeerrViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
private val seerrServerRepository: SeerrServerRepository,
|
||||||
private val seerrService: SeerrService,
|
private val seerrService: SeerrService,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
|
|
@ -28,75 +33,70 @@ class SwitchSeerrViewModel
|
||||||
|
|
||||||
val serverConnectionStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
val serverConnectionStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
private fun cleanUrl(url: String) =
|
|
||||||
if (!url.endsWith("/api/v1")) {
|
|
||||||
url
|
|
||||||
.toHttpUrlOrNull()
|
|
||||||
?.newBuilder()
|
|
||||||
?.apply {
|
|
||||||
addPathSegment("api")
|
|
||||||
addPathSegment("v1")
|
|
||||||
}?.build()
|
|
||||||
.toString()
|
|
||||||
} else {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
fun submitServer(
|
|
||||||
url: String,
|
|
||||||
apiKey: String,
|
|
||||||
) {
|
|
||||||
viewModelScope.launchIO {
|
|
||||||
val url = cleanUrl(url)
|
|
||||||
val result =
|
|
||||||
try {
|
|
||||||
seerrServerRepository.testConnection(
|
|
||||||
authMethod = SeerrAuthMethod.API_KEY,
|
|
||||||
url = url,
|
|
||||||
username = null,
|
|
||||||
passwordOrApiKey = apiKey,
|
|
||||||
)
|
|
||||||
} catch (ex: ClientException) {
|
|
||||||
Timber.w(ex, "Error logging in via API Key")
|
|
||||||
if (ex.statusCode == 401 || ex.statusCode == 403) {
|
|
||||||
LoadingState.Error("Invalid credentials", ex)
|
|
||||||
} else {
|
|
||||||
LoadingState.Error(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result is LoadingState.Success) {
|
|
||||||
seerrServerRepository.addAndChangeServer(url, apiKey)
|
|
||||||
}
|
|
||||||
serverConnectionStatus.update { result }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun submitServer(
|
fun submitServer(
|
||||||
url: String,
|
url: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
passwordOrApiKey: String,
|
||||||
authMethod: SeerrAuthMethod,
|
authMethod: SeerrAuthMethod,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val url = cleanUrl(url)
|
serverConnectionStatus.update { LoadingState.Loading }
|
||||||
val result =
|
val urls =
|
||||||
try {
|
try {
|
||||||
seerrServerRepository.testConnection(
|
createUrls(url)
|
||||||
authMethod = authMethod,
|
} catch (ex: IllegalArgumentException) {
|
||||||
url = url,
|
showToast(context, "Invalid URL")
|
||||||
username = username,
|
serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) }
|
||||||
passwordOrApiKey = password,
|
return@launchIO
|
||||||
)
|
}
|
||||||
} catch (ex: ClientException) {
|
var result: LoadingState = LoadingState.Error("No url")
|
||||||
Timber.w(ex, "Error logging in via %s", authMethod)
|
for (url in urls) {
|
||||||
if (ex.statusCode == 401 || ex.statusCode == 403) {
|
Timber.d("Trying %s", url)
|
||||||
LoadingState.Error("Invalid credentials", ex)
|
result =
|
||||||
} else {
|
try {
|
||||||
|
seerrServerRepository.testConnection(
|
||||||
|
authMethod = authMethod,
|
||||||
|
url = url.toString(),
|
||||||
|
username = username.takeIf { authMethod != SeerrAuthMethod.API_KEY },
|
||||||
|
passwordOrApiKey = passwordOrApiKey,
|
||||||
|
)
|
||||||
|
} catch (ex: ClientException) {
|
||||||
|
Timber.w(ex, "Error logging in")
|
||||||
|
if (ex.statusCode == 401 || ex.statusCode == 403) {
|
||||||
|
showToast(context, "Invalid credentials")
|
||||||
|
LoadingState.Error("Invalid credentials", ex)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
LoadingState.Error("Could not connect with URL")
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
LoadingState.Error(ex)
|
LoadingState.Error(ex)
|
||||||
}
|
}
|
||||||
|
if (result is LoadingState.Success) {
|
||||||
|
when (authMethod) {
|
||||||
|
SeerrAuthMethod.LOCAL,
|
||||||
|
SeerrAuthMethod.JELLYFIN,
|
||||||
|
-> {
|
||||||
|
seerrServerRepository.addAndChangeServer(
|
||||||
|
url.toString(),
|
||||||
|
authMethod,
|
||||||
|
username,
|
||||||
|
passwordOrApiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SeerrAuthMethod.API_KEY -> {
|
||||||
|
seerrServerRepository.addAndChangeServer(
|
||||||
|
url.toString(),
|
||||||
|
passwordOrApiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if (result is LoadingState.Success) {
|
}
|
||||||
seerrServerRepository.addAndChangeServer(url, authMethod, username, password)
|
if (result is LoadingState.Error) {
|
||||||
|
showToast(context, "Error: ${result.message}")
|
||||||
}
|
}
|
||||||
serverConnectionStatus.update { result }
|
serverConnectionStatus.update { result }
|
||||||
}
|
}
|
||||||
|
|
@ -112,3 +112,39 @@ class SwitchSeerrViewModel
|
||||||
serverConnectionStatus.update { LoadingState.Pending }
|
serverConnectionStatus.update { LoadingState.Pending }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createUrls(url: String): List<HttpUrl> {
|
||||||
|
val urls = mutableListOf<String>()
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
urls.add(url)
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) {
|
||||||
|
urls.add("$url:5055")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urls.add("http://$url")
|
||||||
|
val httpUrl = "http://$url".toHttpUrl()
|
||||||
|
if (httpUrl.port == 80) {
|
||||||
|
urls.add("https://$url")
|
||||||
|
urls.add("http://$url:5055")
|
||||||
|
urls.add("https://$url:5055")
|
||||||
|
} else {
|
||||||
|
urls.add("https://$url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls.map { cleanUrl(it).toHttpUrl() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanUrl(url: String) =
|
||||||
|
if (!url.endsWith("/api/v1")) {
|
||||||
|
url
|
||||||
|
.toHttpUrl()
|
||||||
|
.newBuilder()
|
||||||
|
.apply {
|
||||||
|
addPathSegment("api")
|
||||||
|
addPathSegment("v1")
|
||||||
|
}.build()
|
||||||
|
.toString()
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,9 @@ object MPVLib {
|
||||||
|
|
||||||
external fun create(appctx: Context)
|
external fun create(appctx: Context)
|
||||||
|
|
||||||
fun initialize() {
|
external fun init()
|
||||||
synchronized(this) { init() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun init()
|
external fun destroy()
|
||||||
|
|
||||||
fun tearDown() {
|
|
||||||
synchronized(this) { destroy() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun destroy()
|
|
||||||
|
|
||||||
external fun attachSurface(surface: Surface)
|
external fun attachSurface(surface: Surface)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ class MpvPlayer(
|
||||||
SurfaceHolder.Callback {
|
SurfaceHolder.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
||||||
|
private val initLock = Any()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var surface: Surface? = null
|
private var surface: Surface? = null
|
||||||
|
|
@ -131,7 +133,7 @@ class MpvPlayer(
|
||||||
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||||
|
|
||||||
Timber.v("Initializing MPVLib")
|
Timber.v("Initializing MPVLib")
|
||||||
MPVLib.initialize()
|
MPVLib.init()
|
||||||
|
|
||||||
MPVLib.setOptionString("force-window", "no")
|
MPVLib.setOptionString("force-window", "no")
|
||||||
MPVLib.setOptionString("idle", "yes")
|
MPVLib.setOptionString("idle", "yes")
|
||||||
|
|
@ -1086,14 +1088,18 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.INITIALIZE -> {
|
MpvCommand.INITIALIZE -> {
|
||||||
init()
|
synchronized(initLock) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.DESTROY -> {
|
MpvCommand.DESTROY -> {
|
||||||
clearVideoSurfaceView(null)
|
synchronized(initLock) {
|
||||||
MPVLib.removeLogObserver(mpvLogger)
|
MPVLib.setPropertyBoolean("pause", true)
|
||||||
MPVLib.tearDown()
|
MPVLib.removeLogObserver(mpvLogger)
|
||||||
Timber.d("MPVLib destroyed")
|
MPVLib.destroy()
|
||||||
|
Timber.d("MPVLib destroyed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -897,8 +897,6 @@ components:
|
||||||
- Bloopers
|
- Bloopers
|
||||||
site:
|
site:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
|
||||||
- 'YouTube'
|
|
||||||
MovieDetails:
|
MovieDetails:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1226,6 +1224,10 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/WatchProviders'
|
$ref: '#/components/schemas/WatchProviders'
|
||||||
|
relatedVideos:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RelatedVideo'
|
||||||
MediaRequest:
|
MediaRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.github.damontecres.wholphin.test
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.ui.setup.seerr.createUrls
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class TestSeerr {
|
||||||
|
@Test
|
||||||
|
fun testCreateUrls() {
|
||||||
|
val urls =
|
||||||
|
createUrls("jellyseerr.com")
|
||||||
|
.map { it.toString() }
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
listOf(
|
||||||
|
"http://jellyseerr.com/api/v1",
|
||||||
|
"https://jellyseerr.com/api/v1",
|
||||||
|
"http://jellyseerr.com:5055/api/v1",
|
||||||
|
"https://jellyseerr.com:5055/api/v1",
|
||||||
|
)
|
||||||
|
Assert.assertEquals(expected, urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateUrls2() {
|
||||||
|
val urls =
|
||||||
|
createUrls("https://jellyseerr.com")
|
||||||
|
.map { it.toString() }
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
listOf(
|
||||||
|
"https://jellyseerr.com/api/v1",
|
||||||
|
"https://jellyseerr.com:5055/api/v1",
|
||||||
|
)
|
||||||
|
Assert.assertEquals(expected, urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateUrls3() {
|
||||||
|
val urls =
|
||||||
|
createUrls("http://jellyseerr.com")
|
||||||
|
.map { it.toString() }
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
listOf(
|
||||||
|
"http://jellyseerr.com/api/v1",
|
||||||
|
"http://jellyseerr.com:5055/api/v1",
|
||||||
|
)
|
||||||
|
Assert.assertEquals(expected, urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCreateUrls4() {
|
||||||
|
val urls =
|
||||||
|
createUrls("jellyseerr.com:5055")
|
||||||
|
.map { it.toString() }
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
listOf(
|
||||||
|
"http://jellyseerr.com:5055/api/v1",
|
||||||
|
"https://jellyseerr.com:5055/api/v1",
|
||||||
|
)
|
||||||
|
Assert.assertEquals(expected, urls)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue