Skip to content

Commit

Permalink
Add poster translation
Browse files Browse the repository at this point in the history
  • Loading branch information
VSeryi committed May 4, 2024
1 parent a5ddcb9 commit cf15192
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface TmdbRemoteDataSource {

suspend fun fetchShowImages(tmdbId: Long): TmdbImages

suspend fun fetchEpisodeImage(showTmdbId: Long?, season: Int?, episode: Int?): TmdbImage?
suspend fun fetchEpisodeImage(showTmdbId: Long?, season: Int?, episode: Int?): TmdbImages

suspend fun fetchMovieImages(tmdbId: Long): TmdbImages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ internal class TmdbApi(private val service: TmdbService) : TmdbRemoteDataSource
if (showTmdbId == null || showTmdbId <= 0) TmdbImages.EMPTY
if (season == null || season <= 0) TmdbImages.EMPTY
if (episode == null || episode <= 0) TmdbImages.EMPTY
val images = service.fetchEpisodeImages(showTmdbId, season, episode)
images.stills?.firstOrNull()
service.fetchEpisodeImages(showTmdbId, season, episode)
} catch (error: Throwable) {
null
TmdbImages.EMPTY
}

override suspend fun fetchMovieImages(tmdbId: Long) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,16 @@ data class TmdbImage(
fun isPlain() = iso_639_1 == null

fun isEnglish() = iso_639_1 == "en"

fun isLanguage(language: String) = iso_639_1 == language

fun votes(): Double {
val z = 1.96 // Z-score corresponding to a 95% confidence level
val v = vote_count.toDouble()
val m = vote_average.toDouble()
if (v == 0.0) return 0.0
val phat = m / 10.0 // Proportion of average rating out of 10
// Calculate the lower bound of the Wilson score interval: https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval
return (phat + z * z / (2 * v) - z * kotlin.math.sqrt((phat * (1 - phat) + z * z / (4 * v)) / v)) / (1 + z * z / v)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.michaldrabik.repository.images
import com.michaldrabik.common.dispatchers.CoroutineDispatchers
import com.michaldrabik.data_local.LocalDataSource
import com.michaldrabik.data_remote.RemoteDataSource
import com.michaldrabik.data_remote.tmdb.model.TmdbImage
import com.michaldrabik.repository.TranslationsRepository
import com.michaldrabik.repository.mappers.Mappers
import com.michaldrabik.ui_model.Episode
import com.michaldrabik.ui_model.IdTmdb
Expand All @@ -14,7 +16,6 @@ import com.michaldrabik.ui_model.ImageStatus.UNAVAILABLE
import com.michaldrabik.ui_model.ImageType
import com.michaldrabik.ui_model.ImageType.FANART
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -23,40 +24,27 @@ class EpisodeImagesProvider @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val remoteSource: RemoteDataSource,
private val localSource: LocalDataSource,
private val mappers: Mappers
private val mappers: Mappers,
private var translationsRepository: TranslationsRepository
) {

suspend fun loadRemoteImage(showId: IdTmdb, episode: Episode): Image = withContext(dispatchers.IO) {
val tvdbId = episode.ids.tvdb
val tmdbId = episode.ids.tmdb
val cachedImage = findCachedImage(episode, FANART)
val type = FANART
val cachedImage = findCachedImage(episode, type)
if (cachedImage.status == AVAILABLE) {
return@withContext cachedImage
}

var image = Image.createUnavailable(FANART)
try {
var remoteImage = remoteSource.tmdb.fetchEpisodeImage(showId.id, episode.season, episode.number)
if (remoteImage == null && (episode.numberAbs ?: 0) > 0) {
// Try absolute episode number if present (may happen with certain Anime series)
remoteImage = remoteSource.tmdb.fetchEpisodeImage(showId.id, episode.season, episode.numberAbs)
}
image = when (remoteImage) {
null -> Image.createUnavailable(FANART)
else -> Image(
id = -1,
idTvdb = tvdbId,
idTmdb = tmdbId,
type = FANART,
family = EPISODE,
fileUrl = remoteImage.file_path,
thumbnailUrl = "",
status = AVAILABLE,
source = ImageSource.TMDB
)
}
} catch (error: Throwable) {
Timber.w(error)
val typeImages = (remoteSource.tmdb.fetchEpisodeImage(showId.id, episode.season, episode.number).stills
// Try absolute episode number if present (may happen with certain Anime series)
?: (episode.numberAbs?.let { remoteSource.tmdb.fetchEpisodeImage(showId.id, episode.season, it).stills }
?: emptyList()))

val remoteImage = findBestImage(typeImages)
val image = when (remoteImage) {
null -> Image.createUnavailable(FANART)
else -> Image.createAvailable(episode.ids, type, EPISODE, remoteImage.file_path, ImageSource.TMDB)
}

when (image.status) {
Expand All @@ -74,4 +62,14 @@ class EpisodeImagesProvider @Inject constructor(
else -> mappers.image.fromDatabase(cachedImage).copy(type = type)
}
}

private fun findBestImage(images: List<TmdbImage>): TmdbImage? {
val languageCode = translationsRepository.getLanguageCode()
return images.maxWithOrNull(
compareBy<TmdbImage> { it.isLanguage(languageCode) }
.thenBy { it.isEnglish() }
.thenBy { it.isPlain() }
.thenBy { it.votes() }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.michaldrabik.data_local.LocalDataSource
import com.michaldrabik.data_remote.RemoteDataSource
import com.michaldrabik.data_remote.tmdb.model.TmdbImage
import com.michaldrabik.data_remote.tmdb.model.TmdbImages
import com.michaldrabik.repository.TranslationsRepository
import com.michaldrabik.repository.mappers.Mappers
import com.michaldrabik.ui_model.IdTmdb
import com.michaldrabik.ui_model.IdTrakt
Expand All @@ -29,7 +30,8 @@ class MovieImagesProvider @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val remoteSource: RemoteDataSource,
private val localSource: LocalDataSource,
private val mappers: Mappers
private val mappers: Mappers,
private var translationsRepository: TranslationsRepository
) {

private val unavailableCache = mutableSetOf<IdTrakt>()
Expand Down Expand Up @@ -66,7 +68,7 @@ class MovieImagesProvider @Inject constructor(
else -> throw Error("Invalid type")
}

val remoteImage = findBestImage(typeImages, type)
val remoteImage = findBestImage(typeImages)
val image = when (remoteImage) {
null -> Image.createUnavailable(type, MOVIE, TMDB)
else -> Image.createAvailable(movie.ids, type, MOVIE, remoteImage.file_path, TMDB)
Expand Down Expand Up @@ -98,7 +100,7 @@ class MovieImagesProvider @Inject constructor(
FANART, FANART_WIDE -> images.backdrops ?: emptyList()
else -> throw Error("Invalid type")
}
findBestImage(typeImages, extraType)?.let {
findBestImage(typeImages)?.let {
val extraImage = Image(-1, tvdbId, tmdbId, extraType, MOVIE, it.file_path, "", AVAILABLE, TMDB)
localSource.movieImages.insertMovieImage(mappers.image.toDatabaseMovie(extraImage))
}
Expand All @@ -118,13 +120,15 @@ class MovieImagesProvider @Inject constructor(
}
}

private fun findBestImage(images: List<TmdbImage>, type: ImageType) =
images
.filter { if (type == POSTER) it.isEnglish() else it.isPlain() }
.sortedWith(compareBy({ it.vote_count }, { it.vote_average }))
.lastOrNull()
?: images.firstOrNull { if (type == POSTER) it.isEnglish() else it.isPlain() }
?: images.firstOrNull()
private fun findBestImage(images: List<TmdbImage>): TmdbImage? {
val languageCode = translationsRepository.getLanguageCode()
return images.maxWithOrNull(
compareBy<TmdbImage> { it.isLanguage(languageCode) }
.thenBy { it.isEnglish() }
.thenBy { it.isPlain() }
.thenBy { it.votes() }
)
}

suspend fun deleteLocalCache() = withContext(dispatchers.IO) {
localSource.movieImages.deleteAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.michaldrabik.data_remote.RemoteDataSource
import com.michaldrabik.data_remote.aws.model.AwsImages
import com.michaldrabik.data_remote.tmdb.model.TmdbImage
import com.michaldrabik.data_remote.tmdb.model.TmdbImages
import com.michaldrabik.repository.TranslationsRepository
import com.michaldrabik.repository.mappers.Mappers
import com.michaldrabik.ui_model.IdTmdb
import com.michaldrabik.ui_model.IdTrakt
Expand All @@ -32,6 +33,7 @@ class ShowImagesProvider @Inject constructor(
private val remoteSource: RemoteDataSource,
private val localSource: LocalDataSource,
private val mappers: Mappers,
private var translationsRepository: TranslationsRepository
) {

private val unavailableCache = mutableSetOf<IdTrakt>()
Expand Down Expand Up @@ -100,7 +102,7 @@ class ShowImagesProvider @Inject constructor(
val episode = seasons[0].episodes?.firstOrNull()
episode?.let { ep ->
runCatching {
val backupImage = remoteSource.tmdb.fetchEpisodeImage(tmdbId.id, ep.season, ep.number)
val backupImage = remoteSource.tmdb.fetchEpisodeImage(tmdbId.id, ep.season, ep.number).stills?.firstOrNull()
backupImage?.let {
typeImages = listOf(TmdbImage(it.file_path, 0F, 0, "en"))
}
Expand All @@ -110,7 +112,7 @@ class ShowImagesProvider @Inject constructor(
}
}

val remoteImage = findBestImage(typeImages, type)
val remoteImage = findBestImage(typeImages)
val image = when (remoteImage) {
null -> Image.createUnavailable(type)
else -> Image.createAvailable(show.ids, type, SHOW, remoteImage.file_path, source)
Expand Down Expand Up @@ -142,7 +144,7 @@ class ShowImagesProvider @Inject constructor(
FANART, FANART_WIDE -> images.backdrops ?: emptyList()
else -> throw Error("Invalid type")
}
findBestImage(typeImages, extraType)?.let {
findBestImage(typeImages)?.let {
val extraImage = Image(-1, tvdbId, tmdbId, extraType, SHOW, it.file_path, "", AVAILABLE, TMDB)
localSource.showImages.insertShowImage(mappers.image.toDatabaseShow(extraImage))
}
Expand Down Expand Up @@ -170,13 +172,15 @@ class ShowImagesProvider @Inject constructor(
}
}

private fun findBestImage(images: List<TmdbImage>, type: ImageType) =
images
.filter { if (type == POSTER) it.isEnglish() else it.isPlain() }
.sortedWith(compareBy({ it.vote_count }, { it.vote_average }))
.lastOrNull()
?: images.firstOrNull { if (type == POSTER) it.isEnglish() else it.isPlain() }
?: images.firstOrNull()
private fun findBestImage(images: List<TmdbImage>): TmdbImage? {
val languageCode = translationsRepository.getLanguageCode()
return images.maxWithOrNull(
compareBy<TmdbImage> { it.isLanguage(languageCode) }
.thenBy { it.isEnglish() }
.thenBy { it.isPlain() }
.thenBy { it.votes() }
)
}

suspend fun deleteLocalCache() = withContext(dispatchers.IO) {
localSource.showImages.deleteAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ class SettingsRepository @Inject constructor(
movieTranslations.deleteByLanguage(input)
episodesTranslations.deleteByLanguage(input)
people.deleteTranslations()
movieImages.deleteAll()
showImages.deleteAll()
}
}
}
Expand Down

0 comments on commit cf15192

Please sign in to comment.