Use new intermediate model in most places

This commit is contained in:
Niels van Velzen
2025-05-01 22:21:16 +02:00
committed by Niels van Velzen
parent fddbb95a90
commit 68bdf5a28c
12 changed files with 88 additions and 202 deletions

View File

@ -24,6 +24,7 @@ import org.jellyfin.androidtv.auth.model.User
import org.jellyfin.androidtv.auth.store.AuthenticationPreferences
import org.jellyfin.androidtv.auth.store.AuthenticationStore
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.apiclient.primaryImage
import org.jellyfin.androidtv.util.sdk.forUser
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
@ -124,7 +125,7 @@ class AuthenticationRepositoryImpl(
serverId = server.id,
name = userInfo.name!!,
accessToken = result.accessToken,
imageTag = userInfo.primaryImageTag,
imageTag = userInfo.primaryImage?.tag,
lastUsed = Instant.now().toEpochMilli(),
)
@ -165,11 +166,11 @@ class AuthenticationRepositoryImpl(
val updatedUser = currentUser?.copy(
name = userInfo.name!!,
lastUsed = Instant.now().toEpochMilli(),
imageTag = userInfo.primaryImageTag,
imageTag = userInfo.primaryImage?.tag,
accessToken = accessToken,
) ?: AuthenticationStoreUser(
name = userInfo.name!!,
imageTag = userInfo.primaryImageTag,
imageTag = userInfo.primaryImage?.tag,
accessToken = accessToken,
)
authenticationStore.putUser(server.id, userInfo.id, updatedUser)

View File

@ -232,7 +232,7 @@ class LeanbackChannelWorker(
preferParentThumb: Boolean
): Uri = when {
type == BaseItemKind.MOVIE || type == BaseItemKind.SERIES -> itemImages[ImageType.PRIMARY]
(preferParentThumb || imageTags?.contains(ImageType.PRIMARY) != true) && parentThumbItemId != null -> parentImages[ImageType.THUMB]
(preferParentThumb || !itemImages.contains(ImageType.PRIMARY)) && parentImages.contains(ImageType.THUMB) -> parentImages[ImageType.THUMB]
else -> itemImages[ImageType.PRIMARY]
}.let { image ->
ImageProvider.getImageUri(image?.getUrl(api) ?: imageHelper.getResourceUrl(context, R.drawable.tile_land_tv))

View File

@ -96,7 +96,7 @@ class DreamViewModel(
)
val item = response.items.firstOrNull { item ->
!item.backdropImageTags.isNullOrEmpty()
item.itemBackdropImages.isNotEmpty()
} ?: return null
Timber.i("Loading random library showcase item ${item.id}")

View File

@ -416,7 +416,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
mDetailsOverviewRow = new MyDetailsOverviewRow(item);
String primaryImageUrl = imageHelper.getValue().getLogoImageUrl(mBaseItem, 600, true);
String primaryImageUrl = imageHelper.getValue().getLogoImageUrl(mBaseItem, 600);
if (primaryImageUrl == null) {
primaryImageUrl = imageHelper.getValue().getPrimaryImageUrl(mBaseItem, false, null, posterHeight);
}

View File

@ -472,7 +472,7 @@ public class ItemListFragment extends Fragment implements View.OnKeyListener {
private void updateBackdrop() {
BaseItemDto item = mBaseItem;
if (item.getBackdropImageTags() == null || item.getBackdropImageTags().isEmpty() && mItems != null && mItems.size() >= 1)
if (item.getBackdropImageTags() == null || item.getBackdropImageTags().isEmpty() && mItems != null && !mItems.isEmpty())
item = mItems.get(new Random().nextInt(mItems.size()));
backgroundService.getValue().setBackground(item);

View File

@ -6,6 +6,7 @@ import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.TimeUtils
import org.jellyfin.androidtv.util.apiclient.seriesPrimaryImage
import org.jellyfin.androidtv.util.getTimeFormatter
import org.jellyfin.androidtv.util.locale
import org.jellyfin.androidtv.util.sdk.getFullName
@ -126,17 +127,10 @@ open class BaseItemDtoBaseRowItem @JvmOverloads constructor(
fillWidth: Int,
fillHeight: Int
): String? {
val seriesId = baseItem?.seriesId
val seriesPrimaryImageTag = baseItem?.seriesPrimaryImageTag
val seriesPrimaryImage = baseItem?.seriesPrimaryImage
return when {
preferSeriesPoster && seriesId != null && seriesPrimaryImageTag != null -> {
imageHelper.getImageUrl(
seriesId,
org.jellyfin.sdk.model.api.ImageType.PRIMARY,
seriesPrimaryImageTag
)
}
preferSeriesPoster && seriesPrimaryImage != null -> imageHelper.getImageUrl(seriesPrimaryImage)
imageType == ImageType.BANNER -> imageHelper.getBannerImageUrl(
requireNotNull(

View File

@ -21,9 +21,10 @@ import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.databinding.FragmentPictureViewerBinding
import org.jellyfin.androidtv.ui.AsyncImageView
import org.jellyfin.androidtv.ui.ScreensaverViewModel
import org.jellyfin.androidtv.util.apiclient.getUrl
import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.androidtv.util.createKeyHandler
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy
@ -207,20 +208,18 @@ class PictureViewerFragment : Fragment(), View.OnKeyListener {
}
private fun AsyncImageView.load(item: BaseItemDto) {
val url = api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.PRIMARY,
tag = item.imageTags?.get(ImageType.PRIMARY),
// Ask the server to downscale the image to avoid the app going out of memory
// unfortunately this can be a bit slow for larger files
maxWidth = resources.displayMetrics.widthPixels,
maxHeight = resources.displayMetrics.heightPixels,
)
val image = item.itemImages[ImageType.PRIMARY]
load(
url = url,
blurHash = item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(item.imageTags?.get(ImageType.PRIMARY)),
aspectRatio = item.primaryImageAspectRatio ?: 1.0,
url = image?.getUrl(
api = api,
// Ask the server to downscale the image to avoid the app going out of memory
// unfortunately this can be a bit slow for larger files
maxWidth = resources.displayMetrics.widthPixels,
maxHeight = resources.displayMetrics.heightPixels,
),
blurHash = image?.blurHash,
aspectRatio = image?.aspectRatio?.toDouble() ?: 1.0,
)
}
}

View File

@ -1240,7 +1240,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
binding.itemTitle.setText(current.getName());
}
// Update the logo
String imageUrl = imageHelper.getValue().getLogoImageUrl(current, 440, false);
String imageUrl = imageHelper.getValue().getLogoImageUrl(current, 440);
if (imageUrl != null) {
binding.itemLogo.setVisibility(View.VISIBLE);
binding.itemTitle.setVisibility(View.GONE);

View File

@ -24,6 +24,8 @@ import org.jellyfin.androidtv.ui.itemhandling.BaseItemDtoBaseRowItem;
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.util.ImageHelper;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.JellyfinImage;
import org.jellyfin.androidtv.util.apiclient.JellyfinImageKt;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
import org.jellyfin.sdk.model.api.UserItemDataDto;
@ -31,7 +33,6 @@ import org.koin.java.KoinJavaComponent;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Map;
import kotlin.Lazy;
@ -370,26 +371,20 @@ public class CardPresenter extends Presenter {
}
}
String blurHash = null;
if (rowItem.getBaseItem() != null && rowItem.getBaseItem().getImageBlurHashes() != null) {
Map<String, String> blurHashMap;
String imageTag;
JellyfinImage image = null;
if (rowItem.getBaseItem() != null) {
if (aspect == ImageHelper.ASPECT_RATIO_BANNER) {
blurHashMap = rowItem.getBaseItem().getImageBlurHashes().get(org.jellyfin.sdk.model.api.ImageType.BANNER);
imageTag = rowItem.getBaseItem().getImageTags().get(org.jellyfin.sdk.model.api.ImageType.BANNER);
image = JellyfinImageKt.getItemImages(rowItem.getBaseItem()).get(org.jellyfin.sdk.model.api.ImageType.BANNER);
} else if (aspect == ImageHelper.ASPECT_RATIO_2_3 && rowItem.getBaseItem().getType() == BaseItemKind.EPISODE && rowItem instanceof BaseItemDtoBaseRowItem && ((BaseItemDtoBaseRowItem) rowItem).getPreferSeriesPoster()) {
blurHashMap = rowItem.getBaseItem().getImageBlurHashes().get(org.jellyfin.sdk.model.api.ImageType.PRIMARY);
imageTag = rowItem.getBaseItem().getSeriesPrimaryImageTag();
image = JellyfinImageKt.getSeriesPrimaryImage(rowItem.getBaseItem());
} else if (aspect == ImageHelper.ASPECT_RATIO_16_9 && !isUserView && (rowItem.getBaseItem().getType() != BaseItemKind.EPISODE || !rowItem.getBaseItem().getImageTags().containsKey(org.jellyfin.sdk.model.api.ImageType.PRIMARY) || (rowItem.getPreferParentThumb() && rowItem.getBaseItem().getParentThumbImageTag() != null))) {
blurHashMap = rowItem.getBaseItem().getImageBlurHashes().get(org.jellyfin.sdk.model.api.ImageType.THUMB);
imageTag = (rowItem.getPreferParentThumb() || !rowItem.getBaseItem().getImageTags().containsKey(org.jellyfin.sdk.model.api.ImageType.PRIMARY)) ? rowItem.getBaseItem().getParentThumbImageTag() : rowItem.getBaseItem().getImageTags().get(org.jellyfin.sdk.model.api.ImageType.THUMB);
if (rowItem.getPreferParentThumb() || !rowItem.getBaseItem().getImageTags().containsKey(org.jellyfin.sdk.model.api.ImageType.PRIMARY)) {
image = JellyfinImageKt.getParentImages(rowItem.getBaseItem()).get(org.jellyfin.sdk.model.api.ImageType.THUMB);
} else {
image = JellyfinImageKt.getItemImages(rowItem.getBaseItem()).get(org.jellyfin.sdk.model.api.ImageType.THUMB);
}
} else {
blurHashMap = rowItem.getBaseItem().getImageBlurHashes().get(org.jellyfin.sdk.model.api.ImageType.PRIMARY);
imageTag = rowItem.getBaseItem().getImageTags().get(org.jellyfin.sdk.model.api.ImageType.PRIMARY);
}
if (blurHashMap != null && !blurHashMap.isEmpty() && imageTag != null && blurHashMap.get(imageTag) != null) {
blurHash = blurHashMap.get(imageTag);
image = JellyfinImageKt.getItemImages(rowItem.getBaseItem()).get(org.jellyfin.sdk.model.api.ImageType.PRIMARY);
}
}
@ -397,8 +392,8 @@ public class CardPresenter extends Presenter {
int fillHeight = Math.round(holder.getCardHeight() * holder.mCardView.getResources().getDisplayMetrics().density);
holder.updateCardViewImage(
rowItem.getImageUrl(holder.mCardView.getContext(), imageHelper.getValue(), mImageType, fillWidth, fillHeight),
blurHash
image == null ? rowItem.getImageUrl(holder.mCardView.getContext(), imageHelper.getValue(), mImageType, fillWidth, fillHeight) : imageHelper.getValue().getImageUrl(image),
image == null ? null : image.getBlurHash()
);
}

View File

@ -9,6 +9,7 @@ import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.card.LegacyImageCardView
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.sdk.model.api.ImageType
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -25,12 +26,10 @@ class UserViewCardPresenter(
val baseItem = rowItem?.baseItem
// Load image
val imageTag = baseItem?.imageTags?.get(ImageType.PRIMARY)
val imageBlurhash = imageTag?.let { baseItem.imageBlurHashes?.get(ImageType.PRIMARY)?.get(it) }
val imageUrl = imageTag?.let { imageHelper.getImageUrl(baseItem.id, ImageType.PRIMARY, it) }
val image = baseItem?.itemImages[ImageType.PRIMARY]
cardView.mainImageView.load(
url = imageUrl,
blurHash = imageBlurhash,
url = image?.let(imageHelper::getImageUrl),
blurHash = image?.blurHash,
placeholder = ContextCompat.getDrawable(cardView.context, R.drawable.tile_land_folder),
aspectRatio = ImageHelper.ASPECT_RATIO_16_9,
blurHashResolution = 32,

View File

@ -4,14 +4,20 @@ import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.annotation.AnyRes
import org.jellyfin.androidtv.util.apiclient.JellyfinImage
import org.jellyfin.androidtv.util.apiclient.albumPrimaryImage
import org.jellyfin.androidtv.util.apiclient.getUrl
import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.androidtv.util.apiclient.parentImages
import org.jellyfin.androidtv.util.apiclient.primaryImage
import org.jellyfin.androidtv.util.apiclient.seriesPrimaryImage
import org.jellyfin.androidtv.util.apiclient.seriesThumbImage
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.UserDto
import java.util.UUID
class ImageHelper(
private val api: ApiClient,
@ -25,6 +31,8 @@ class ImageHelper(
const val MAX_PRIMARY_IMAGE_HEIGHT: Int = 370
}
fun getImageUrl(image: JellyfinImage): String = image.getUrl(api)
fun getImageAspectRatio(item: BaseItemDto, preferParentThumb: Boolean): Double {
if (preferParentThumb && (item.parentThumbItemId != null || item.seriesThumbImageTag != null)) {
return ASPECT_RATIO_16_9
@ -40,94 +48,37 @@ class ImageHelper(
return primaryAspectRatio ?: ASPECT_RATIO_7_9
}
fun getPrimaryImageUrl(item: BaseItemPerson, maxHeight: Int? = null): String? {
if (item.primaryImageTag == null) return null
fun getPrimaryImageUrl(
item: BaseItemPerson,
maxHeight: Int? = null,
): String? = item.primaryImage?.getUrl(api, maxHeight = maxHeight)
return item.id.let { itemId ->
api.imageApi.getItemImageUrl(
itemId = itemId,
imageType = ImageType.PRIMARY,
tag = item.primaryImageTag,
maxHeight = maxHeight,
)
}
}
fun getPrimaryImageUrl(
item: UserDto,
): String? = item.primaryImage?.getUrl(api)
fun getPrimaryImageUrl(item: UserDto): String? {
if (item.primaryImageTag == null) return null
return api.imageApi.getUserImageUrl(
userId = item.id,
tag = item.primaryImageTag,
maxHeight = MAX_PRIMARY_IMAGE_HEIGHT,
)
}
fun getPrimaryImageUrl(item: BaseItemDto, width: Int? = null, height: Int? = null): String? {
val primaryImageTag = item.imageTags?.get(ImageType.PRIMARY) ?: return null
return api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.PRIMARY,
tag = primaryImageTag,
maxWidth = width,
maxHeight = height,
)
}
fun getImageUrl(itemId: UUID, imageType: ImageType, imageTag: String): String =
api.imageApi.getItemImageUrl(
itemId = itemId,
imageType = imageType,
tag = imageTag,
maxHeight = MAX_PRIMARY_IMAGE_HEIGHT,
)
fun getPrimaryImageUrl(
item: BaseItemDto,
width: Int? = null,
height: Int? = null,
): String? = item.itemImages[ImageType.PRIMARY]?.getUrl(api, maxWidth = width, maxHeight = height)
fun getPrimaryImageUrl(
item: BaseItemDto,
preferParentThumb: Boolean,
fillWidth: Int? = null,
fillHeight: Int? = null
): String {
var itemId = item.id
var imageTag = item.imageTags?.get(ImageType.PRIMARY)
var imageType = ImageType.PRIMARY
): String? {
val image = when {
preferParentThumb && item.type == BaseItemKind.EPISODE -> item.parentImages[ImageType.THUMB] ?: item.seriesThumbImage
item.type == BaseItemKind.SEASON -> item.seriesPrimaryImage
item.type == BaseItemKind.PROGRAM && item.imageTags?.containsKey(ImageType.THUMB) == true -> item.itemImages[ImageType.THUMB]
item.type == BaseItemKind.AUDIO -> item.albumPrimaryImage
else -> null
} ?: item.itemImages[ImageType.PRIMARY]
if (preferParentThumb && item.type == BaseItemKind.EPISODE) {
if (item.parentThumbItemId != null && item.parentThumbImageTag != null) {
itemId = item.parentThumbItemId!!
imageTag = item.parentThumbImageTag
imageType = ImageType.THUMB
} else if (item.seriesId != null && item.seriesThumbImageTag != null) {
itemId = item.seriesId!!
imageTag = item.seriesThumbImageTag
imageType = ImageType.THUMB
}
} else if (item.type == BaseItemKind.SEASON && imageTag == null) {
if (item.seriesId != null && item.seriesPrimaryImageTag != null) {
itemId = item.seriesId!!
imageTag = item.seriesPrimaryImageTag
}
} else if (item.type == BaseItemKind.PROGRAM && item.imageTags?.containsKey(ImageType.THUMB) == true) {
imageTag = item.imageTags!![ImageType.THUMB]
imageType = ImageType.THUMB
} else if (item.type == BaseItemKind.AUDIO && imageTag == null) {
if (item.albumId != null && item.albumPrimaryImageTag != null) {
itemId = item.albumId!!
imageTag = item.albumPrimaryImageTag
} else if (!item.artistItems.isNullOrEmpty()) {
itemId = item.artistItems!!.first().id
imageTag = null
} else if (!item.albumArtists.isNullOrEmpty()) {
itemId = item.albumArtists!!.first().id
imageTag = null
}
}
return api.imageApi.getItemImageUrl(
itemId = itemId,
imageType = imageType,
tag = imageTag,
return image?.getUrl(
api = api,
fillWidth = fillWidth,
fillHeight = fillHeight,
)
@ -135,76 +86,22 @@ class ImageHelper(
fun getLogoImageUrl(
item: BaseItemDto?,
maxWidth: Int? = null,
useSeriesFallback: Boolean = true
maxWidth: Int? = null
): String? {
val logoTag = item?.imageTags?.get(ImageType.LOGO)
return when {
// No item
item == null -> null
// Item has a logo
logoTag != null -> {
api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.LOGO,
maxWidth = maxWidth,
tag = logoTag
)
}
// Item parent has a logo
item.parentLogoItemId != null && item.parentLogoImageTag != null -> {
api.imageApi.getItemImageUrl(
itemId = item.parentLogoItemId!!,
imageType = ImageType.LOGO,
maxWidth = maxWidth,
tag = item.parentLogoImageTag
)
}
// Series might have a logo
useSeriesFallback && item.seriesId != null -> {
api.imageApi.getItemImageUrl(
itemId = item.seriesId!!,
imageType = ImageType.LOGO,
maxWidth = maxWidth,
)
}
else -> null
}
val image = item?.itemImages[ImageType.LOGO] ?: item?.parentImages[ImageType.LOGO]
return image?.getUrl(api, maxWidth = maxWidth)
}
fun getThumbImageUrl(item: BaseItemDto, fillWidth: Int, fillHeight: Int): String {
val thumbTag = item.imageTags?.get(ImageType.THUMB)
return if (thumbTag == null) {
getPrimaryImageUrl(item, true, fillWidth, fillHeight)
} else {
api.imageApi.getItemImageUrl(
itemId = item.id,
tag = thumbTag,
imageType = ImageType.THUMB,
fillWidth = fillWidth,
fillHeight = fillHeight,
)
}
}
fun getBannerImageUrl(item: BaseItemDto, fillWidth: Int, fillHeight: Int): String {
val bannerTag = item.imageTags?.get(ImageType.BANNER)
return if (bannerTag == null) {
getPrimaryImageUrl(item, true, fillWidth, fillHeight)
} else {
api.imageApi.getItemImageUrl(
itemId = item.id,
tag = bannerTag,
imageType = ImageType.BANNER,
fillWidth = fillWidth,
fillHeight = fillHeight,
)
}
}
fun getThumbImageUrl(
item: BaseItemDto,
fillWidth: Int,
fillHeight: Int,
): String? = item.itemImages[ImageType.THUMB]?.getUrl(api, fillWidth = fillWidth, fillHeight = fillHeight)
?: getPrimaryImageUrl(item, true, fillWidth, fillHeight)
fun getBannerImageUrl(item: BaseItemDto, fillWidth: Int, fillHeight: Int): String? =
item.itemImages[ImageType.BANNER]?.getUrl(api, fillWidth = fillWidth, fillHeight = fillHeight)
?: getPrimaryImageUrl(item, true, fillWidth, fillHeight)
/**
* A utility to return a URL reference to an image resource

View File

@ -4,6 +4,7 @@ package org.jellyfin.androidtv.util.sdk
import org.jellyfin.androidtv.auth.model.PublicUser
import org.jellyfin.androidtv.auth.model.Server
import org.jellyfin.androidtv.util.apiclient.primaryImage
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.serializer.toUUID
@ -21,6 +22,6 @@ fun UserDto.toPublicUser(): PublicUser? {
name = name ?: return null,
serverId = serverId?.toUUIDOrNull() ?: return null,
accessToken = null,
imageTag = primaryImageTag
imageTag = primaryImage?.tag
)
}