Skip to content

Commit

Permalink
Merge pull request #1071 from UweTrottmann/higher-resolution-episode-…
Browse files Browse the repository at this point in the history
…images

Higher resolution episode images
  • Loading branch information
UweTrottmann authored Nov 21, 2024
2 parents ba8dad2 + 75dbb5d commit 043a2de
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Releases marked with 🧪 (or previously with the "beta" suffix) were released o
### Unreleased

* 🌟 Shows: add a note to a show, synced with SeriesGuide Cloud or Trakt (VIP only).
* 🔧 Shows: increase resolution of episode images.
* 🔧 Shows: also use plus symbol for button on discover screen to be consistent.

### 2024.5.0 - 2024-11-06 🧪
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,21 @@ object TmdbSettings {
private const val KEY_TMDB_BASE_URL = "com.battlelancer.seriesguide.tmdb.baseurl"
const val POSTER_SIZE_SPEC_W154 = "w154"
const val POSTER_SIZE_SPEC_W342 = "w342"
private const val STILL_SIZE_SPEC_W300 = "w300"

/**
* As of 2024-11:
*
* - w300 (300 × 169 px) JPEG is around 9-16 kB
* - w780 (780 × 439 px) JPEG is around 30-70 kB
* - w1280 (1280 × 720 px) JPEG is around 60-140 KB
*
* Samples:
*
* - https://image.tmdb.org/t/p/original/8Tvnx22rzhwArofFPhmcfaBvgjN.jpg (smallest)
* - https://image.tmdb.org/t/p/original/j4WEC9Jh4AyXF8ynpX3pz633tse.jpg
* - https://image.tmdb.org/t/p/original/5Bh7EE3p6OOS0NzH22AE0N7DYO8.jpg (largest)
*/
const val BACKDROP_SMALL_SIZE_SPEC = "w780"
private const val IMAGE_SIZE_SPEC_ORIGINAL = "original"
const val DEFAULT_BASE_URL = "https://image.tmdb.org/t/p/"

Expand Down Expand Up @@ -69,7 +83,7 @@ object TmdbSettings {
}
}

fun getStillUrl(context: Context, path: String): String {
return getImageBaseUrl(context) + STILL_SIZE_SPEC_W300 + path
fun buildBackdropUrl(context: Context, path: String): String {
return "${getImageBaseUrl(context)}$BACKDROP_SMALL_SIZE_SPEC$path"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import com.battlelancer.seriesguide.ui.BaseMessageActivity.ServiceActiveEvent
import com.battlelancer.seriesguide.ui.BaseMessageActivity.ServiceCompletedEvent
import com.battlelancer.seriesguide.ui.FullscreenImageActivity.Companion.intent
import com.battlelancer.seriesguide.util.ImageTools
import com.battlelancer.seriesguide.util.ImageTools.tmdbOrTvdbStillUrl
import com.battlelancer.seriesguide.util.LanguageTools
import com.battlelancer.seriesguide.util.RatingsTools.initialize
import com.battlelancer.seriesguide.util.RatingsTools.setLink
Expand Down Expand Up @@ -451,8 +450,8 @@ class EpisodeDetailsFragment : Fragment(), EpisodeActionsContract {
binding.containerImage.setOnClickListener { v: View? ->
val intent = intent(
requireContext(),
tmdbOrTvdbStillUrl(imagePath, requireContext(), false),
tmdbOrTvdbStillUrl(imagePath, requireContext(), true)
ImageTools.buildEpisodeImageUrl(imagePath, requireContext()),
ImageTools.buildEpisodeImageUrl(imagePath, requireContext(), originalSize = true)
)
Utils.startActivityWithAnimation(requireActivity(), intent, v)
}
Expand Down Expand Up @@ -685,7 +684,7 @@ class EpisodeDetailsFragment : Fragment(), EpisodeActionsContract {
binding.containerImage.visibility = View.VISIBLE
ImageTools.loadWithPicasso(
requireContext(),
tmdbOrTvdbStillUrl(imagePath, requireContext(), false)
ImageTools.buildEpisodeImageUrl(imagePath, requireContext())
)
.error(R.drawable.ic_photo_gray_24dp)
.into(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import com.battlelancer.seriesguide.traktapi.TraktTools
import com.battlelancer.seriesguide.ui.BaseMessageActivity.ServiceActiveEvent
import com.battlelancer.seriesguide.ui.BaseMessageActivity.ServiceCompletedEvent
import com.battlelancer.seriesguide.util.ImageTools
import com.battlelancer.seriesguide.util.ImageTools.tmdbOrTvdbStillUrl
import com.battlelancer.seriesguide.util.LanguageTools
import com.battlelancer.seriesguide.util.RatingsTools.initialize
import com.battlelancer.seriesguide.util.RatingsTools.setLink
Expand Down Expand Up @@ -650,7 +649,7 @@ class OverviewFragment() : Fragment(), EpisodeActionsContract {
// Try loading image
ImageTools.loadWithPicasso(
requireContext(),
tmdbOrTvdbStillUrl(imagePath, requireContext(), false)
ImageTools.buildEpisodeImageUrl(imagePath, requireContext())
)
.error(R.drawable.ic_photo_gray_24dp)
.into(imageView,
Expand Down
83 changes: 44 additions & 39 deletions app/src/main/java/com/battlelancer/seriesguide/util/ImageTools.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2023 Uwe Trottmann
// SPDX-License-Identifier: Apache-2.0
// Copyright 2021-2024 Uwe Trottmann

package com.battlelancer.seriesguide.util

Expand Down Expand Up @@ -71,45 +71,24 @@ object ImageTools {
return tmdbOrTvdbPosterUrl(imagePath, context)
}

/**
* Calls [buildTmdbOrTvdbImageCacheUrl] with small image and demo URLs for show posters.
*/
@JvmStatic
fun tmdbOrTvdbPosterUrl(
imagePath: String?,
context: Context,
originalSize: Boolean = false
): String? {
return if (imagePath.isNullOrEmpty()) {
null
} else {
if (AppSettings.isDemoModeEnabled(context)) {
return pickDemoPosterUrl(imagePath)
return buildTmdbOrTvdbImageCacheUrl(
imagePath, context, originalSize,
demoUrl = { nonNullImagePath ->
pickDemoPosterUrl(nonNullImagePath)
},
tmdbSmallImageUrl = { nonNullImagePath ->
"${TmdbSettings.getPosterBaseUrl(context)}$nonNullImagePath"
}

// If the path contains the legacy TVDB cache prefix, use the www subdomain as it has
// a redirect to the new thumbnail URL set up (artworks subdomain + file name postfix).
// E.g. https://www.thetvdb.com/banners/_cache/posters/example.jpg redirects to
// https://artworks.thetvdb.com/banners/posters/example_t.jpg
// Using the artworks subdomain with the legacy cache prefix is not supported.
val imageUrl = when {
imagePath.contains(TVDB_LEGACY_CACHE_PREFIX, false) -> {
"${TVDB_LEGACY_MIRROR_BANNERS}$imagePath"
}

imagePath.startsWith("/") -> {
// TMDB images have no path at all, but always start with /.
// Use small size based on density, or original size (as large as possible).
if (originalSize) {
TmdbSettings.getImageOriginalUrl(context, imagePath)
} else {
"${TmdbSettings.getPosterBaseUrl(context)}$imagePath"
}
}

else -> {
"${TVDB_MIRROR_BANNERS}$imagePath"
}
}
buildImageCacheUrl(imageUrl)
}
)
}

private val demoPosterUrls = listOf(
Expand All @@ -123,24 +102,50 @@ object ImageTools {
"https://seriesgui.de/demo/sitcom.jpg",
)

private val demoStillUrl = "https://seriesgui.de/demo/episode-anime.jpg"
private val demoEpisodeImageUrl = "https://seriesgui.de/demo/episode-anime.jpg"

private fun pickDemoPosterUrl(imagePath: String): String {
// Map an image path always to the same image
return demoPosterUrls[imagePath.hashCode().mod(demoPosterUrls.size)]
}

@JvmStatic
fun tmdbOrTvdbStillUrl(
/**
* Calls [buildTmdbOrTvdbImageCacheUrl] with small image and demo URLs for episode images.
*/
fun buildEpisodeImageUrl(
imagePath: String?,
context: Context,
originalSize: Boolean = false
): String? {
return buildTmdbOrTvdbImageCacheUrl(
imagePath, context, originalSize,
demoUrl = { demoEpisodeImageUrl },
tmdbSmallImageUrl = { nonNullImagePath ->
TmdbSettings.buildBackdropUrl(context, nonNullImagePath)
}
)
}

/**
* Builds an image cache URL, or returns null if [imagePath] is null or empty.
*
* Returns [demoUrl] if [AppSettings.isDemoModeEnabled] is enabled.
*
* If [imagePath] starts with `/` builds a TMDB episode image path with resolution depending on
* [originalSize]. Otherwise a legacy TVDB URL.
*/
private fun buildTmdbOrTvdbImageCacheUrl(
imagePath: String?,
context: Context,
originalSize: Boolean = false,
demoUrl: (String) -> String,
tmdbSmallImageUrl: (String) -> String
): String? {
return if (imagePath.isNullOrEmpty()) {
null
} else {
if (AppSettings.isDemoModeEnabled(context)) {
return demoStillUrl
return demoUrl(imagePath)
}

// If the path contains the legacy TVDB cache prefix, use the www subdomain as it has
Expand All @@ -159,7 +164,7 @@ object ImageTools {
if (originalSize) {
TmdbSettings.getImageOriginalUrl(context, imagePath)
} else {
TmdbSettings.getStillUrl(context, imagePath)
tmdbSmallImageUrl(imagePath)
}
}

Expand All @@ -174,7 +179,7 @@ object ImageTools {
/**
* [posterUrl] must not be empty.
*/
fun buildImageCacheUrl(posterUrl: String): String? {
private fun buildImageCacheUrl(posterUrl: String): String? {
@Suppress("SENSELESS_COMPARISON")
if (BuildConfig.IMAGE_CACHE_URL == null) {
return posterUrl // no cache
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright 2023 Uwe Trottmann
// SPDX-License-Identifier: Apache-2.0
// Copyright 2021-2024 Uwe Trottmann

package com.battlelancer.seriesguide.util

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.battlelancer.seriesguide.EmptyTestApplication
import com.battlelancer.seriesguide.settings.TmdbSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -20,15 +21,51 @@ class ImageToolsTest {

@Test
fun posterUrl() {
assertImageUrl(
TmdbSettings.POSTER_SIZE_SPEC_W154,
tmdbUrlBuilder = { path ->
ImageTools.tmdbOrTvdbPosterUrl(path, context)
},
tmdbUrlOriginalBuilder = { path ->
ImageTools.tmdbOrTvdbPosterUrl(path, context, true)
},
tvdbUrlBuilder = { path ->
ImageTools.tmdbOrTvdbPosterUrl(path, context)
}
)
}

@Test
fun episodeImageUrl() {
assertImageUrl(
TmdbSettings.BACKDROP_SMALL_SIZE_SPEC,
tmdbUrlBuilder = { path ->
ImageTools.buildEpisodeImageUrl(path, context)
},
tmdbUrlOriginalBuilder = { path ->
ImageTools.buildEpisodeImageUrl(path, context, originalSize = true)
},
tvdbUrlBuilder = { path ->
ImageTools.buildEpisodeImageUrl(path, context)
}
)
}

private fun assertImageUrl(
smallSize: String,
tmdbUrlBuilder: (String) -> String?,
tmdbUrlOriginalBuilder: (String) -> String?,
tvdbUrlBuilder: (String) -> String?
) {
// Note: TMDB image paths start with / whereas TVDB paths do not.
val tmdbUrl = ImageTools.tmdbOrTvdbPosterUrl("/example.jpg", context)
val tmdbUrlOriginal = ImageTools.tmdbOrTvdbPosterUrl("/example.jpg", context, true)
val tvdbUrl = ImageTools.tmdbOrTvdbPosterUrl("posters/example.jpg", context)
val tmdbUrl = tmdbUrlBuilder("/example.jpg")
val tmdbUrlOriginal = tmdbUrlOriginalBuilder("/example.jpg")
val tvdbUrl = tvdbUrlBuilder("posters/example.jpg")
println("TMDB URL: $tmdbUrl")
println("TMDB original URL: $tmdbUrlOriginal")
println("TVDB URL: $tvdbUrl")
assertThat(tmdbUrl).isNotEmpty()
assertThat(tmdbUrl).endsWith("https://image.tmdb.org/t/p/w154/example.jpg")
assertThat(tmdbUrl).endsWith("https://image.tmdb.org/t/p/$smallSize/example.jpg")
assertThat(tmdbUrlOriginal).isNotEmpty()
assertThat(tmdbUrlOriginal).endsWith("https://image.tmdb.org/t/p/original/example.jpg")
assertThat(tvdbUrl).isNotEmpty()
Expand Down

0 comments on commit 043a2de

Please sign in to comment.