Several minor bug fixes (#1811)

## Description
Fixes several small bugs and does some minor refactoring:
- Use "Recently Added in XYZ" instead of "Recently recorded" for
recorded tv libraries
- Improve home page query performance by using fewer fields in the query
- When an error occurs connecting to discovered servers, show the error
instead of only logging
- Fix headers sometimes appearing briefly on some pages when navigating
back
- Catch exceptions in socket connections
- Use a larger buffer when downloading updates
- Split up ADB logging for media info

### Related issues
Related to
https://github.com/damontecres/Wholphin/issues/1517#issuecomment-5184316086
Maybe the larger download buffer will help with
https://github.com/damontecres/Wholphin/issues/1696
Will help show the error in #1801
Prevents the crash in #1786

### Testing
Emulator & nvidia shield

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-08-13 12:36:20 -04:00 • committed by GitHub
parent 54966f9124
commit 51ab39ed3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 282 additions and 250 deletions

View file

@ -12,9 +12,8 @@ import com.github.damontecres.wholphin.data.model.createGenreDestination
import com.github.damontecres.wholphin.data.model.createStudioDestination
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
import com.github.damontecres.wholphin.preferences.HomePagePreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.HomeItemFields
import com.github.damontecres.wholphin.ui.ProgramItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
import com.github.damontecres.wholphin.ui.main.settings.Library
import com.github.damontecres.wholphin.ui.main.settings.favoriteOptions
@ -62,6 +61,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.GetProgramsDto
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.UserDto
@ -280,7 +280,7 @@ class HomeSettingsService
libraries
.mapIndexed { index, it ->
val parentId = it.itemId
val title = getRecentlyAddedTitle(it)
val title = getRecentlyAddedTitle(it.name)
if (it.collectionType == CollectionType.LIVETV) {
HomeRowConfigDisplay(
id = index,
@ -766,13 +766,11 @@ class HomeSettingsService
}
is HomeRowConfig.RecentlyAdded -> {
val library =
libraries
.firstOrNull { it.itemId == row.parentId }
val title = getRecentlyAddedTitle(library)
val library = libraries.firstOrNull { it.itemId == row.parentId }
val title = getRecentlyAddedTitle(library?.name)
val request =
GetLatestMediaRequest(
fields = SlimItemFields,
fields = library.itemFields,
imageTypeLimit = 1,
parentId = row.parentId,
groupItems = true,
@ -797,12 +795,9 @@ class HomeSettingsService
}
is HomeRowConfig.RecentlyReleased -> {
val name =
libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val library = libraries.firstOrNull { it.itemId == row.parentId }
val title =
name?.let {
library?.name?.let {
ResArgStringProvider(R.string.recently_released_in, it)
} ?: ResStringProvider(R.string.recently_released)
val request =
@ -821,7 +816,7 @@ class HomeSettingsService
SortOrder.ASCENDING,
SortOrder.DESCENDING,
),
fields = DefaultItemFields,
fields = library.itemFields,
recursive = true,
maxPremiereDate = LocalDateTime.now(),
isUnaired = false,
@ -851,6 +846,7 @@ class HomeSettingsService
}
is HomeRowConfig.ByParent -> {
val library = libraries.firstOrNull { it.itemId == row.parentId }
val request =
GetItemsRequest(
userId = userDto.id,
@ -885,7 +881,7 @@ class HomeSettingsService
}
},
limit = limit,
fields = DefaultItemFields,
fields = library.itemFields,
)
// Not using getItemName because we want to throw the 404
@ -970,7 +966,7 @@ class HomeSettingsService
GetPersonsRequest(
userId = userDto.id,
limit = limit,
fields = DefaultItemFields,
fields = HomeItemFields,
isFavorite = true,
enableImages = true,
enableImageTypes = listOf(ImageType.PRIMARY),
@ -988,12 +984,18 @@ class HomeSettingsService
)
}
} else {
val fields =
if (row.kind == BaseItemKind.BOX_SET) {
HomeItemFieldsBoxSets
} else {
HomeItemFields
}
val request =
GetItemsRequest(
userId = userDto.id,
recursive = true,
limit = limit,
fields = DefaultItemFields,
fields = fields,
includeItemTypes = listOf(row.kind),
isFavorite = true,
)
@ -1027,7 +1029,7 @@ class HomeSettingsService
GetRecordingsRequest(
userId = userDto.id,
isInProgress = true,
fields = DefaultItemFields,
fields = HomeItemFields,
limit = limit,
enableImages = true,
enableUserData = true,
@ -1098,7 +1100,7 @@ class HomeSettingsService
val request =
GetLiveTvChannelsRequest(
userId = userDto.id,
fields = DefaultItemFields,
fields = HomeItemFields,
limit = limit,
enableImages = true,
)
@ -1225,10 +1227,17 @@ class UnsupportedHomeSettingsVersionException(
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
fun getRecentlyAddedTitle(library: Library?): StringProvider =
if (library?.isRecordingFolder == true) {
ResStringProvider(R.string.recently_recorded)
} else {
library?.name?.let { ResArgStringProvider(R.string.recently_added_in, it) }
?: ResStringProvider(R.string.recently_added)
}
fun getRecentlyAddedTitle(name: String?): StringProvider =
name?.let { ResArgStringProvider(R.string.recently_added_in, it) }
?: ResStringProvider(R.string.recently_added)
private val Library?.itemFields: List<ItemFields>
get() =
if (this?.type == BaseItemKind.COLLECTION_FOLDER && this.collectionType == CollectionType.BOXSETS) {
// Get child count to show in the header
HomeItemFieldsBoxSets
} else {
HomeItemFields
}
private val HomeItemFieldsBoxSets get() = HomeItemFields + listOf(ItemFields.CHILD_COUNT)

View file

@ -7,6 +7,7 @@ import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.google.protobuf.MessageLiteOrBuilder
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.Json
@ -76,21 +77,18 @@ class MediaReportService
model=${Build.MODEL}
apiLevel=${Build.VERSION.SDK_INT}
playbackPrefs=${
appPreferences.playbackPreferences.toString().replace("\n", ", ")
.replace("\t", " ")
}
experimental=${
appPreferences.experimentalPreferences.toString().replace("\n", ", ")
.replace("\t", " ")
}
deviceProfile=$deviceProfileJson
playbackPrefs=${appPreferences.playbackPreferences.toStringOneLine()}
experimental=${appPreferences.experimentalPreferences.toStringOneLine()}
mediaSources=$sourcesJson
deviceProfile=$deviceProfileJson
""".trimIndent()
Timber.w(body)
body.chunked(2048).forEach { Timber.w(it) }
Timber.w("End send media info")
val response by api.clientLogApi.logFile(body)
showToast(context, "Sent! Filename=${response.fileName}")
}
}
private fun MessageLiteOrBuilder.toStringOneLine(): String = toString().replace("\n", ", ").replace("\t", " ")

View file

@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.ui.showToast
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.jellyfin.sdk.api.client.ApiClient
@ -94,6 +95,8 @@ class ServerEventListener
.joinToString("\n")
showToast(context, toast, Toast.LENGTH_LONG)
}
}.catch { ex ->
Timber.e(ex, "Error in websocket subscription")
}.launchIn(activity.lifecycleScope)
}

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.services
import android.Manifest
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.content.Intent
@ -61,8 +62,6 @@ class UpdateChecker
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
private const val PERMISSION_REQUEST_CODE = 123456
private val NOTE_REGEX = Regex("<!-- app-note:(.+) -->")
val ACTIVE = BuildConfig.UPDATING_ENABLED
@ -158,8 +157,8 @@ class UpdateChecker
private fun getRelease(request: Request): Release? {
return okHttpClient.newCall(request).execute().use {
if (it.isSuccessful && it.body != null) {
val result = Json.parseToJsonElement(it.body!!.string())
if (it.isSuccessful) {
val result = Json.parseToJsonElement(it.body.string())
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
val version = Version.tryFromString(name)
val publishedAt =
@ -195,6 +194,7 @@ class UpdateChecker
/**
* Download and install an update
*/
@SuppressLint("RequestInstallPackagesPolicy")
suspend fun installRelease(
release: Release,
callback: DownloadCallback,
@ -208,7 +208,7 @@ class UpdateChecker
.get()
.build()
okHttpClient.newCall(request).execute().use {
if (it.isSuccessful && it.body != null) {
if (it.isSuccessful) {
Timber.v("Request successful for ${release.downloadUrl}")
withContext(WholphinDispatchers.Main) {
callback.contentLength(it.body.contentLength())
@ -230,7 +230,7 @@ class UpdateChecker
contentValues,
)
if (uri != null) {
it.body!!.byteStream().use { input ->
it.body.byteStream().use { input ->
resolver.openOutputStream(uri).use { output ->
copyTo(input, output!!, callback = callback)
}
@ -290,7 +290,7 @@ class UpdateChecker
downloadDir.mkdirs()
val targetFile = File(downloadDir, APK_NAME)
targetFile.outputStream().use { output ->
response.body!!.byteStream().use { input ->
response.body.byteStream().use { input ->
copyTo(input, output, callback = callback)
}
}
@ -374,7 +374,7 @@ data class Release(
Regex("https://github.com/\\w*/\\w+/pull/(\\d+)"),
"#$1",
)
// Remove the last line for full changelog since its just a link
// Remove the last line for full changelog since it's just a link
.replace(Regex("\\*\\*Full Changelog\\*\\*.*"), "")
}
@ -387,29 +387,30 @@ interface DownloadCallback {
suspend fun copyTo(
input: InputStream,
out: OutputStream,
bufferSize: Int = 16 * 1024,
bufferSize: Int = 64 * 1024,
callback: DownloadCallback,
): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = input.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
withContext(WholphinDispatchers.Main) {
callback.bytesDownloaded(bytesCopied)
): Long =
withContext(WholphinDispatchers.IO) {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = input.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
withContext(WholphinDispatchers.Main) {
callback.bytesDownloaded(bytesCopied)
}
bytes = input.read(buffer)
}
bytes = input.read(buffer)
return@withContext bytesCopied
}
return bytesCopied
}
fun getDownloadUrl(
assets: JsonArray,
debug: Boolean,
supportedAbis: List<String> = Build.SUPPORTED_ABIS.toList(),
supportedABIs: List<String> = Build.SUPPORTED_ABIS.toList(),
): String? {
val abiSuffix = supportedAbis.firstOrNull().let { if (it != null) "-$it" else "" }
val abiSuffix = supportedABIs.firstOrNull().let { if (it != null) "-$it" else "" }
val releaseSuffix = if (debug) "-debug" else "-release"
val preferredNames =
buildList {