Add image caching (#65)

* Add caching to settings with ability to choose cache size

* Remove unused parameter from Api

* Add glide module for cache setup

* Clean up image handling in adapters

* Move caching to it's own category

Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
lsrom 2021-11-27 18:46:08 +01:00 committed by GitHub
parent e259c405bb
commit 94b3790560
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 106 deletions

View file

@ -1,11 +1,22 @@
package dev.jdtech.jellyfin package dev.jdtech.jellyfin
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import dev.jdtech.jellyfin.adapters.* import dev.jdtech.jellyfin.adapters.CollectionListAdapter
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
import dev.jdtech.jellyfin.adapters.EpisodeItem
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
import dev.jdtech.jellyfin.adapters.FavoritesListAdapter
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
import dev.jdtech.jellyfin.adapters.HomeItem
import dev.jdtech.jellyfin.adapters.PersonListAdapter
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.adapters.ViewListAdapter
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.models.DownloadSection import dev.jdtech.jellyfin.models.DownloadSection
@ -35,44 +46,26 @@ fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
@BindingAdapter("itemImage") @BindingAdapter("itemImage")
fun bindItemImage(imageView: ImageView, item: BaseItemDto) { fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
val itemId = val itemId =
if (item.type == "Episode" || item.type == "Season" && item.imageTags.isNullOrEmpty()) item.seriesId else item.id if (item.type == "Episode" || item.type == "Season" && item.imageTags.isNullOrEmpty()) item.seriesId else item.id
Glide imageView
.with(imageView.context) .loadImage("/items/$itemId/Images/${ImageType.PRIMARY}")
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.PRIMARY}")) .posterDescription(item.name)
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.into(imageView)
imageView.contentDescription = "${item.name} poster"
} }
@BindingAdapter("itemBackdropImage") @BindingAdapter("itemBackdropImage")
fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) { fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) {
if (item == null) return if (item == null) return
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
Glide imageView
.with(imageView.context) .loadImage("/items/${item.id}/Images/${ImageType.BACKDROP}")
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/${ImageType.BACKDROP}")) .backdropDescription(item.name)
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView)
imageView.contentDescription = "${item.name} backdrop"
} }
@BindingAdapter("itemBackdropById") @BindingAdapter("itemBackdropById")
fun bindItemBackdropById(imageView: ImageView, itemId: UUID) { fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") imageView.loadImage("/items/$itemId/ MediaStore.Images /${ImageType.BACKDROP}")
Glide
.with(imageView.context)
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.BACKDROP}"))
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView)
} }
@BindingAdapter("collections") @BindingAdapter("collections")
@ -89,17 +82,9 @@ fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
@BindingAdapter("personImage") @BindingAdapter("personImage")
fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) { fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") imageView
.loadImage("/items/${person.id}/Images/${ImageType.PRIMARY}")
Glide .posterDescription(person.name)
.with(imageView.context)
.load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/${ImageType.PRIMARY}"))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.error(R.drawable.person_placeholder)
.into(imageView)
imageView.contentDescription = "${person.name} poster"
} }
@BindingAdapter("episodes") @BindingAdapter("episodes")
@ -118,8 +103,6 @@ fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) { fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
if (episode == null) return if (episode == null) return
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
var imageItemId = episode.id var imageItemId = episode.id
var imageType = ImageType.PRIMARY var imageType = ImageType.PRIMARY
@ -143,26 +126,14 @@ fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
} }
} }
Glide imageView
.with(imageView.context) .loadImage("/items/${imageItemId}/Images/$imageType")
.load(jellyfinApi.api.baseUrl.plus("/items/${imageItemId}/Images/$imageType")) .posterDescription(episode.name)
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.into(imageView)
imageView.contentDescription = "${episode.name} poster"
} }
@BindingAdapter("seasonPoster") @BindingAdapter("seasonPoster")
fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) { fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "") imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}")
Glide
.with(imageView.context)
.load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/${ImageType.PRIMARY}"))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.into(imageView)
} }
@BindingAdapter("favoriteSections") @BindingAdapter("favoriteSections")
@ -176,3 +147,24 @@ fun bindDownloadSections(recyclerView: RecyclerView, data: List<DownloadSection>
val adapter = recyclerView.adapter as DownloadsListAdapter val adapter = recyclerView.adapter as DownloadsListAdapter
adapter.submitList(data) adapter.submitList(data)
} }
private fun ImageView.loadImage(url: String, errorPlaceHolderId: Int? = null): View {
val api = JellyfinApi.getInstance(context.applicationContext)
return Glide
.with(context)
.load("${api.api.baseUrl}$url")
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.also { if (errorPlaceHolderId != null) error(errorPlaceHolderId) }
.into(this)
.view
}
private fun View.posterDescription(name: String?) {
contentDescription = String.format(context.resources.getString(R.string.image_description_poster), name)
}
private fun View.backdropDescription(name: String?) {
contentDescription = String.format(context.resources.getString(R.string.image_description_backdrop), name)
}

View file

@ -24,18 +24,13 @@ import java.util.UUID
* @param baseUrl The url of the server * @param baseUrl The url of the server
* @constructor Creates a new [JellyfinApi] instance * @constructor Creates a new [JellyfinApi] instance
*/ */
class JellyfinApi(androidContext: Context, baseUrl: String) { class JellyfinApi(androidContext: Context) {
val jellyfin = createJellyfin { val jellyfin = createJellyfin {
clientInfo = ClientInfo( clientInfo =
name = androidContext.applicationInfo.loadLabel(androidContext.packageManager) ClientInfo(name = androidContext.applicationInfo.loadLabel(androidContext.packageManager).toString(), version = BuildConfig.VERSION_NAME)
.toString(),
version = BuildConfig.VERSION_NAME
)
context = androidContext context = androidContext
} }
val api = jellyfin.createApi()
val api = jellyfin.createApi(baseUrl = baseUrl)
var userId: UUID? = null var userId: UUID? = null
val devicesApi = api.devicesApi val devicesApi = api.devicesApi
@ -54,37 +49,15 @@ class JellyfinApi(androidContext: Context, baseUrl: String) {
@Volatile @Volatile
private var INSTANCE: JellyfinApi? = null private var INSTANCE: JellyfinApi? = null
/** fun getInstance(context: Context): JellyfinApi {
* Creates or gets a new instance of [JellyfinApi]
*
* If there already is an instance, it will return that instance and ignore the [baseUrl] parameter
*
* @param context The context
* @param baseUrl The url of the server
*/
fun getInstance(context: Context, baseUrl: String): JellyfinApi {
synchronized(this) { synchronized(this) {
var instance = INSTANCE var instance = INSTANCE
if (instance == null) { if (instance == null) {
instance = JellyfinApi(context.applicationContext, baseUrl) instance = JellyfinApi(context.applicationContext)
INSTANCE = instance INSTANCE = instance
} }
return instance return instance
} }
} }
/**
* Create a new [JellyfinApi] instance
*
* @param context The context
* @param baseUrl The url of the server
*/
fun newInstance(context: Context, baseUrl: String): JellyfinApi {
synchronized(this) {
val instance = JellyfinApi(context.applicationContext, baseUrl)
INSTANCE = instance
return instance
}
}
} }
} }

View file

@ -9,7 +9,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import java.util.* import java.util.UUID
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -22,7 +22,7 @@ object ApiModule {
sharedPreferences: SharedPreferences, sharedPreferences: SharedPreferences,
serverDatabase: ServerDatabaseDao serverDatabase: ServerDatabaseDao
): JellyfinApi { ): JellyfinApi {
val jellyfinApi = JellyfinApi.getInstance(application, "") val jellyfinApi = JellyfinApi.getInstance(application)
val serverId = sharedPreferences.getString("selectedServer", null) val serverId = sharedPreferences.getString("selectedServer", null)
if (serverId != null) { if (serverId != null) {

View file

@ -0,0 +1,51 @@
package dev.jdtech.jellyfin.di
import android.content.Context
import androidx.preference.PreferenceManager
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.engine.DiskCacheStrategy.NONE
import com.bumptech.glide.load.engine.DiskCacheStrategy.RESOURCE
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
private const val cacheDefaultSize = 250
@GlideModule
class GlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val use = preferences.getBoolean("use_image_cache", false)
if (use) {
val sizeMb = preferences.getString("image_cache_size", "$cacheDefaultSize")?.toInt()!!
val sizeB = 1024L * 1024 * sizeMb
Timber.d("Setting image cache to use $sizeMb MB of disk space")
builder.setDiskCache(InternalCacheDiskCacheFactory(context, sizeB))
builder.caching(enabled = true)
} else {
builder.caching(enabled = false)
Timber.d("Image cache disabled. Clearing all persisted data.")
MainScope().launch {
GlideApp.getPhotoCacheDir(context)?.also {
if (it.exists()) it.deleteRecursively()
}
}
}
}
private fun GlideBuilder.caching(enabled: Boolean) {
setDefaultRequestOptions(
RequestOptions().diskCacheStrategy(
if (enabled) RESOURCE else NONE
)
)
}
}

View file

@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.fragments
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
@ -53,6 +54,10 @@ class SettingsFragment: PreferenceFragmentCompat() {
true true
} }
findPreference<EditTextPreference>("image_cache_size")?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
}
findPreference<EditTextPreference>("deviceName")?.setOnPreferenceChangeListener { _, name -> findPreference<EditTextPreference>("deviceName")?.setOnPreferenceChangeListener { _, name ->
viewModel.updateDeviceName(name.toString()) viewModel.updateDeviceName(name.toString())
true true

View file

@ -55,8 +55,12 @@
<string name="settings_category_player">Přehrávač</string> <string name="settings_category_player">Přehrávač</string>
<string name="manage_servers">Spravovat servery</string> <string name="manage_servers">Spravovat servery</string>
<string name="settings_category_appearance">Vzhled</string> <string name="settings_category_appearance">Vzhled</string>
<string name="settings_category_device">Zařízení</string>
<string name="device_name">Název zařízení</string> <string name="device_name">Název zařízení</string>
<string name="settings_category_device">Zařízení</string>
<string name="settings_use_cache_title">Cachovat obrázky</string>
<string name="settings_use_cache_summary">Cachování obrázků urychluje načítání obsahu. Projeví se po restartu aplikace.</string>
<string name="settings_cache_size">Nastavit velikost cache v MB</string>
<string name="settings_cache_size_message">Aplikace použije tuto velikost v MB k uložení obrázků z Jellyfin serveru. Větší hodnota může být prospěšná na pomalejším připojení.</string>
<string name="theme">Téma</string> <string name="theme">Téma</string>
<string name="error_preparing_player_items">Chyba při načítání přehrávání.</string> <string name="error_preparing_player_items">Chyba při načítání přehrávání.</string>
<string name="view_details">Zobrazit detaily</string> <string name="view_details">Zobrazit detaily</string>
@ -71,4 +75,6 @@
<string name="mpv_player_summary">Použít experimentální MPV přehrávač k přehrávání videí. MPV má podporu pro více video a audio kodeků a formátů titulků.</string> <string name="mpv_player_summary">Použít experimentální MPV přehrávač k přehrávání videí. MPV má podporu pro více video a audio kodeků a formátů titulků.</string>
<string name="force_software_decoding">Vynutit softwarové dekódování</string> <string name="force_software_decoding">Vynutit softwarové dekódování</string>
<string name="force_software_decoding_summary">Vypnout hardwarové dekódování a vynutit softwarové dekódování. Může být užitečné pokud hardwarové dekódování produkuje artefakty.</string> <string name="force_software_decoding_summary">Vypnout hardwarové dekódování a vynutit softwarové dekódování. Může být užitečné pokud hardwarové dekódování produkuje artefakty.</string>
<string name="image_description_poster">%1$s plakát</string>
<string name="image_description_backdrop">%1$s pozadí</string>
</resources> </resources>

View file

@ -59,8 +59,13 @@
<string name="settings_category_player">Player</string> <string name="settings_category_player">Player</string>
<string name="manage_servers">Manage servers</string> <string name="manage_servers">Manage servers</string>
<string name="settings_category_appearance">Appearance</string> <string name="settings_category_appearance">Appearance</string>
<string name="settings_category_device">Device</string>
<string name="device_name">Device name</string> <string name="device_name">Device name</string>
<string name="settings_category_device">Device</string>
<string name="settings_category_cache">Cache</string>
<string name="settings_use_cache_title">Cache images</string>
<string name="settings_use_cache_summary">Cache images on disk to speed up loading times. Will take effect after app restart.</string>
<string name="settings_cache_size">Cache size (MB)</string>
<string name="settings_cache_size_message">App will use this amount of MB of your disk space to store images from Jellyfin server. Larger values might be beneficial on slower networks.</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="error_preparing_player_items">Error preparing player items.</string> <string name="error_preparing_player_items">Error preparing player items.</string>
<string name="view_details">View details</string> <string name="view_details">View details</string>
@ -89,6 +94,9 @@
<string name="sort_order">Sort order</string> <string name="sort_order">Sort order</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="image_description_poster">%1$s poster</string>
<string name="image_description_backdrop">%1$s backdrop</string>
<string-array name="sort_by_options"> <string-array name="sort_by_options">
<item>Name</item> <item>Name</item>
<item>IMDB Rating</item> <item>IMDB Rating</item>

View file

@ -7,28 +7,28 @@
app:defaultValue="null" app:defaultValue="null"
app:entries="@array/languages" app:entries="@array/languages"
app:entryValues="@array/languages_values" app:entryValues="@array/languages_values"
app:icon="@drawable/ic_speaker"
app:key="audio_language" app:key="audio_language"
app:title="@string/settings_preferred_audio_language" app:title="@string/settings_preferred_audio_language"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true" />
app:icon="@drawable/ic_speaker"/>
<ListPreference <ListPreference
app:defaultValue="null" app:defaultValue="null"
app:entries="@array/languages" app:entries="@array/languages"
app:entryValues="@array/languages_values" app:entryValues="@array/languages_values"
app:icon="@drawable/ic_closed_caption"
app:key="subtitle_language" app:key="subtitle_language"
app:title="@string/settings_preferred_subtitle_language" app:title="@string/settings_preferred_subtitle_language"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true" />
app:icon="@drawable/ic_closed_caption"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_servers"> <PreferenceCategory app:title="@string/settings_category_servers">
<Preference <Preference
app:icon="@drawable/ic_server"
app:key="switchServer" app:key="switchServer"
app:title="@string/manage_servers" app:title="@string/manage_servers" />
app:icon="@drawable/ic_server"/>
</PreferenceCategory> </PreferenceCategory>
@ -37,29 +37,43 @@
app:defaultValue="system" app:defaultValue="system"
app:entries="@array/themes" app:entries="@array/themes"
app:entryValues="@array/themes_value" app:entryValues="@array/themes_value"
app:icon="@drawable/ic_palette"
app:key="theme" app:key="theme"
app:title="@string/theme" app:title="@string/theme"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true" />
app:icon="@drawable/ic_palette"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_player"> <PreferenceCategory app:title="@string/settings_category_player">
<SwitchPreference <SwitchPreference
app:key="mpv_player" app:key="mpv_player"
app:title="@string/mpv_player" app:summary="@string/mpv_player_summary"
app:summary="@string/mpv_player_summary"/> app:title="@string/mpv_player" />
<SwitchPreference <SwitchPreference
app:key="mpv_disable_hwdec"
app:dependency="mpv_player" app:dependency="mpv_player"
app:title="@string/force_software_decoding" app:key="mpv_disable_hwdec"
app:summary="@string/force_software_decoding_summary"/> app:summary="@string/force_software_decoding_summary"
app:title="@string/force_software_decoding" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_device"> <PreferenceCategory app:title="@string/settings_category_device">
<EditTextPreference <EditTextPreference
app:key="deviceName" app:key="deviceName"
app:title="@string/device_name" app:title="@string/device_name"
app:useSimpleSummaryProvider="true"/> app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_cache">
<SwitchPreference
app:key="use_image_cache"
app:summary="@string/settings_use_cache_summary"
app:title="@string/settings_use_cache_title" />
<EditTextPreference
app:defaultValue="250"
app:dependency="use_image_cache"
app:dialogMessage="@string/settings_cache_size_message"
app:key="image_cache_size"
app:title="@string/settings_cache_size"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/about"> <PreferenceCategory app:title="@string/about">