Reduce card image transfers and list metadata payloads
This commit is contained in:
parent
f4fb6e22c9
commit
e340d470c1
10 changed files with 152 additions and 31 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.net.Uri
|
||||
import coil3.ComponentRegistry
|
||||
import coil3.ImageLoader
|
||||
|
|
@ -23,6 +25,8 @@ object WolfskinImages {
|
|||
private var networkConfiguration: (ComponentRegistry.Builder.() -> Unit)? = null
|
||||
private const val MEBIBYTE = 1024L * 1024L
|
||||
private const val IMAGE_READS = 4
|
||||
private const val CARD_QUALITY = "80"
|
||||
private const val HERO_QUALITY = "96"
|
||||
private val readPermits = Semaphore(IMAGE_READS)
|
||||
|
||||
/** The app supplies authenticated networking without exposing its server/session to design code. */
|
||||
|
|
@ -78,32 +82,34 @@ object WolfskinImages {
|
|||
|
||||
private val jellyfinImagePath = Regex("/Items/[^/]+/Images/[^/]+(?:/\\d+)?", RegexOption.IGNORE_CASE)
|
||||
private val tmdbSizePath = Regex("/t/p/(?:w\\d+|original)/")
|
||||
private val dimensionParameters = setOf("width", "height", "maxwidth", "maxheight", "fillwidth", "fillheight")
|
||||
private val imageParameters = setOf("width", "height", "maxwidth", "maxheight", "fillwidth", "fillheight", "quality", "format")
|
||||
// Lossy WebP is mandatory from API 14; minSdk is 23. Newer platforms expose a decoder capability query.
|
||||
private val supportsWebp by lazy { Build.VERSION.SDK_INT < 28 || ImageDecoder.isMimeTypeSupported("image/webp") }
|
||||
|
||||
private fun sourceForSize(source: Any, width: Int, height: Int): Any {
|
||||
if (source !is String) return source
|
||||
val uri = Uri.parse(source)
|
||||
if (uri.scheme != "https" && uri.scheme != "http") return source
|
||||
if (uri.host == "image.tmdb.org") {
|
||||
val size = if (height > width) when {
|
||||
width <= 500 -> "w500"
|
||||
width <= 780 -> "w780"
|
||||
else -> "original"
|
||||
} else when {
|
||||
width <= 780 -> "w780"
|
||||
width <= 1280 -> "w1280"
|
||||
else -> "original"
|
||||
val size = when {
|
||||
height >= width -> "w342"
|
||||
width > WolfskinTokens.Image.wideRequestWidth -> "w1280"
|
||||
else -> "w780"
|
||||
}
|
||||
return uri.buildUpon().path(uri.path?.replace(tmdbSizePath, "/t/p/$size/")).build().toString()
|
||||
}
|
||||
if (!jellyfinImagePath.matches(uri.path.orEmpty())) return source
|
||||
val hero = width == WolfskinTokens.Image.heroRequestWidth && height == WolfskinTokens.Image.heroRequestHeight ||
|
||||
width == WolfskinTokens.Image.heroPosterRequestWidth && height == WolfskinTokens.Image.heroPosterRequestHeight
|
||||
val builder = uri.buildUpon().clearQuery()
|
||||
for (name in uri.queryParameterNames) {
|
||||
if (name.lowercase() !in dimensionParameters) {
|
||||
if (name.lowercase() !in imageParameters) {
|
||||
uri.getQueryParameters(name).forEach { value -> builder.appendQueryParameter(name, value) }
|
||||
}
|
||||
}
|
||||
return builder.appendQueryParameter("fillWidth", width.toString())
|
||||
.appendQueryParameter("fillHeight", height.toString()).build().toString()
|
||||
.appendQueryParameter("fillHeight", height.toString())
|
||||
.appendQueryParameter("format", if (hero || !supportsWebp) "Jpg" else "Webp")
|
||||
.appendQueryParameter("quality", if (hero) HERO_QUALITY else CARD_QUALITY).build().toString()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,10 +220,10 @@ object WolfskinTokens {
|
|||
object Image {
|
||||
const val posterRequestWidth = 256
|
||||
const val posterRequestHeight = 384
|
||||
const val tileRequestWidth = 416
|
||||
const val tileRequestHeight = 234
|
||||
const val wideRequestWidth = 560
|
||||
const val wideRequestHeight = 315
|
||||
const val tileRequestWidth = 400
|
||||
const val tileRequestHeight = 225
|
||||
const val wideRequestWidth = 544
|
||||
const val wideRequestHeight = 306
|
||||
const val heroRequestWidth = 1920
|
||||
const val heroRequestHeight = 1080
|
||||
const val heroPosterRequestWidth = 420
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Card URLs bound server work and transfer size, while hero quality and cache identity stay intact. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class)
|
||||
class WolfskinImagesTest {
|
||||
private data class Surface(val name: String, val width: Int, val height: Int, val tier: String, val hero: Boolean = false)
|
||||
private val surfaces = listOf(
|
||||
Surface("grid/Home/Discover/person poster", 256, 384, "w342"),
|
||||
Surface("Home tile", 400, 225, "w780"),
|
||||
Surface("Home wide", 544, 306, "w780"),
|
||||
Surface("episode still", 400, 280, "w780"),
|
||||
Surface("cast portrait", 200, 200, "w342"),
|
||||
Surface("person portrait", 240, 240, "w342"),
|
||||
Surface("hero poster", 420, 630, "w342", true),
|
||||
Surface("Home/Discover/title hero", 1920, 1080, "w1280", true),
|
||||
)
|
||||
|
||||
private fun request(source: Any, surface: Surface): Any = WolfskinImages.request(
|
||||
RuntimeEnvironment.getApplication(), source, surface.width, surface.height, surface.height > surface.width).data
|
||||
|
||||
@Test fun jellyfinSurfacesReplaceStaleDimensionsAndEncodingWithoutLosingIdentity() {
|
||||
for (surface in surfaces) {
|
||||
val source = "https://jelly.example/Items/123/Images/Backdrop/0?tag=stable&api_key=kept&MaxWidth=1280&height=720&FillHeight=720&Quality=96&Format=Png"
|
||||
val rewritten = request(source, surface).toString()
|
||||
val uri = Uri.parse(rewritten)
|
||||
assertEquals(surface.name, surface.width.toString(), uri.getQueryParameter("fillWidth"))
|
||||
assertEquals(surface.name, surface.height.toString(), uri.getQueryParameter("fillHeight"))
|
||||
assertEquals(surface.name, if (surface.hero) "Jpg" else "Webp", uri.getQueryParameter("format"))
|
||||
assertEquals(surface.name, if (surface.hero) "96" else "80", uri.getQueryParameter("quality"))
|
||||
assertEquals("stable", uri.getQueryParameter("tag"))
|
||||
assertEquals("kept", uri.getQueryParameter("api_key"))
|
||||
assertEquals(setOf("tag", "api_key", "fillWidth", "fillHeight", "format", "quality"), uri.queryParameterNames)
|
||||
assertEquals(rewritten, request(rewritten, surface))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun tmdbSurfacesNeverDownloadOriginalArtwork() {
|
||||
for (surface in surfaces) {
|
||||
for (oldTier in listOf("original", "w500", "w780")) {
|
||||
assertEquals(surface.name, "https://image.tmdb.org/t/p/${surface.tier}/art.jpg?identity=kept",
|
||||
request("https://image.tmdb.org/t/p/$oldTier/art.jpg?identity=kept", surface))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun localAndUnrelatedSourcesRemainUntouched() {
|
||||
for (source in listOf("file:///android_asset/poster.webp", "https://cdn.example/poster.jpg?width=800", "https://jelly.example/Users/123/Images/Primary", Uri.parse("content://art/1"))) {
|
||||
assertEquals(source, request(source, surfaces.first()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1492,8 +1492,8 @@
|
|||
},
|
||||
"tileRequest": {
|
||||
"px": [
|
||||
416,
|
||||
234
|
||||
400,
|
||||
225
|
||||
],
|
||||
"decode": "ARGB_8888",
|
||||
"source": "index.html:549",
|
||||
|
|
@ -1501,8 +1501,8 @@
|
|||
},
|
||||
"wideRequest": {
|
||||
"px": [
|
||||
560,
|
||||
315
|
||||
544,
|
||||
306
|
||||
],
|
||||
"decode": "ARGB_8888",
|
||||
"source": "index.html:401",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ b3f096b692c395534177cc82ac68f5b801870bc7e28f0dd5cd7c17e2103920a3 discover-token
|
|||
85109c394255f6120a621fbefe8e58df82832974ccd6a6a975a61567f641ea2a generate_tokens.py
|
||||
25985087986a46f00e2819917d0421ff2b437ec21c54f6ce6412b84c0eb05302 home-tokens.json
|
||||
be3af0ddcb0d67abef0805d19d7a33cdb55428bcace656b50f0a38935f212028 shell-tokens.json
|
||||
bb64e45654fce2cd3ba595b505fb8b5a540526cef696dfa974be25f028d3e119 src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
580ed1710468ccbd97082afaa445e96ae71510ebace711cc735ea389ecb9249d src/main/java/org/justinvisser/wolfskin/design/WolfskinTokens.kt
|
||||
e0a2f5df2f249bce3f6e13f4a0320c7b18b85777eaf04a27a428d6683a6aad28 title-tokens.json
|
||||
36d5b2646dff2e4055d2683e4c284111a6886c311e9c924b73061fa785bc863d tokens.css
|
||||
3226c6fb9c7e35c5bde90aaf01630b238cfcb9a768ff60c49461e19a200c9125 tokens.json
|
||||
226f4bd07c0b105f8640beff71fe2c0c95d0f8862e624cea511384a4591b44ad tokens.json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue