diff --git a/CHANGELOG.md b/CHANGELOG.md index afaaa73bc1..bf50f988cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ๐Ÿงช diff --git a/app/src/main/java/com/battlelancer/seriesguide/settings/TmdbSettings.kt b/app/src/main/java/com/battlelancer/seriesguide/settings/TmdbSettings.kt index 8af3c9741b..7766a4defb 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/settings/TmdbSettings.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/settings/TmdbSettings.kt @@ -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/" @@ -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" } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/episodes/EpisodeDetailsFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/episodes/EpisodeDetailsFragment.kt index c90acb61c9..88b5393cca 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/episodes/EpisodeDetailsFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/episodes/EpisodeDetailsFragment.kt @@ -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 @@ -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) } @@ -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( diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/OverviewFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/OverviewFragment.kt index 329b57f053..b2022f24cd 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/OverviewFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/OverviewFragment.kt @@ -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 @@ -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, diff --git a/app/src/main/java/com/battlelancer/seriesguide/util/ImageTools.kt b/app/src/main/java/com/battlelancer/seriesguide/util/ImageTools.kt index 72780f0ff6..00adfbdfd5 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/util/ImageTools.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/util/ImageTools.kt @@ -1,5 +1,5 @@ -// Copyright 2023 Uwe Trottmann // SPDX-License-Identifier: Apache-2.0 +// Copyright 2021-2024 Uwe Trottmann package com.battlelancer.seriesguide.util @@ -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( @@ -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 @@ -159,7 +164,7 @@ object ImageTools { if (originalSize) { TmdbSettings.getImageOriginalUrl(context, imagePath) } else { - TmdbSettings.getStillUrl(context, imagePath) + tmdbSmallImageUrl(imagePath) } } @@ -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 diff --git a/app/src/test/java/com/battlelancer/seriesguide/util/ImageToolsTest.kt b/app/src/test/java/com/battlelancer/seriesguide/util/ImageToolsTest.kt index a8e2ac81de..1ac09951ab 100644 --- a/app/src/test/java/com/battlelancer/seriesguide/util/ImageToolsTest.kt +++ b/app/src/test/java/com/battlelancer/seriesguide/util/ImageToolsTest.kt @@ -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 @@ -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()