Skip to content
This repository has been archived by the owner on Nov 12, 2024. It is now read-only.

Commit

Permalink
Display season information in Shows Seasons pager (#1580)
Browse files Browse the repository at this point in the history
* Split out season/episode data sources

* Fetch seasons and episodes from TMDb too

* Display season poster

* Fix tests
  • Loading branch information
chrisbanes authored Oct 8, 2023
1 parent 250dacd commit f07cf1e
Show file tree
Hide file tree
Showing 24 changed files with 549 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ interface ImageLoadingComponent : ImageLoadingPlatformComponent {

@Provides
@IntoSet
fun provideEpisodeCoilInterceptor(interceptor: EpisodeImageModelInterceptor): Interceptor = interceptor
fun provideEpisodeInterceptor(interceptor: EpisodeImageModelInterceptor): Interceptor = interceptor

@Provides
@IntoSet
fun provideSeasonInterceptor(interceptor: SeasonImageModelInterceptor): Interceptor = interceptor

@Provides
@IntoSet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

package app.tivi.common.imageloading

import androidx.compose.ui.unit.Density
import app.tivi.data.episodes.SeasonsEpisodesRepository
import app.tivi.data.imagemodels.SeasonImageModel
import app.tivi.data.util.inPast
import app.tivi.tmdb.TmdbImageUrlProvider
import com.seiko.imageloader.intercept.Interceptor
import com.seiko.imageloader.model.ImageRequest
import com.seiko.imageloader.model.ImageResult
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.days
import me.tatarka.inject.annotations.Inject

@Inject
class SeasonImageModelInterceptor(
private val tmdbImageUrlProvider: Lazy<TmdbImageUrlProvider>,
private val repository: SeasonsEpisodesRepository,
private val density: () -> Density,
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = when (val data = chain.request.data) {
is SeasonImageModel -> handle(chain, data)
else -> chain.request
}
return chain.proceed(request)
}

private suspend fun handle(chain: Interceptor.Chain, model: SeasonImageModel): ImageRequest {
if (repository.needSeasonUpdate(model.id, expiry = 180.days.inPast)) {
runCatching { repository.updateSeason(model.id) }
}

val season = repository.getSeason(model.id)
return season?.tmdbPosterPath?.let { posterPath ->
val size = chain.options.sizeResolver.run { density().size() }
val url = tmdbImageUrlProvider.value.getPosterUrl(
path = posterPath,
imageWidth = size.width.roundToInt(),
)

ImageRequest(chain.request) {
data(url)
}
} ?: chain.request
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow
fun ExpandingText(
text: String,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.bodyMedium,
style: TextStyle = MaterialTheme.typography.bodyMedium,
expandable: Boolean = true,
collapsedMaxLines: Int = 4,
expandedMaxLines: Int = Int.MAX_VALUE,
Expand All @@ -31,7 +31,7 @@ fun ExpandingText(

Text(
text = text,
style = textStyle,
style = style,
overflow = TextOverflow.Ellipsis,
maxLines = if (expanded) expandedMaxLines else collapsedMaxLines,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@

package app.tivi.data.episodes

import app.tivi.data.episodes.datasource.EpisodeDataSource
import app.tivi.data.episodes.datasource.EpisodeWatchesDataSource
import app.tivi.data.episodes.datasource.SeasonsEpisodesDataSource
import app.tivi.data.episodes.datasource.TmdbEpisodeDataSourceImpl
import app.tivi.data.episodes.datasource.TmdbSeasonsEpisodesDataSourceImpl
import app.tivi.data.episodes.datasource.TraktEpisodeDataSourceImpl
import app.tivi.data.episodes.datasource.TraktEpisodeWatchesDataSource
import app.tivi.data.episodes.datasource.TraktSeasonsEpisodesDataSourceImpl
import me.tatarka.inject.annotations.Provides

interface EpisodeBinds {
Expand All @@ -19,9 +27,22 @@ interface EpisodeBinds {

@Provides
fun provideTraktSeasonsEpisodesDataSource(
bind: TraktSeasonsEpisodesDataSource,
): SeasonsEpisodesDataSource = bind
bind: TraktSeasonsEpisodesDataSourceImpl,
): TraktSeasonsEpisodesDataSource = bind

@Provides
fun provideTmdbSeasonsEpisodesDataSource(
bind: TmdbSeasonsEpisodesDataSourceImpl,
): TmdbSeasonsEpisodesDataSource = bind

@Provides
fun provideEpisodeWatchesDataSource(
bind: TraktEpisodeWatchesDataSource,
): EpisodeWatchesDataSource = bind
}

typealias TmdbEpisodeDataSource = EpisodeDataSource
typealias TraktEpisodeDataSource = EpisodeDataSource

typealias TmdbSeasonsEpisodesDataSource = SeasonsEpisodesDataSource
typealias TraktSeasonsEpisodesDataSource = SeasonsEpisodesDataSource

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

package app.tivi.data.episodes

import app.tivi.data.daos.LastRequestDao
import app.tivi.data.lastrequests.EntityLastRequestStore
import app.tivi.data.models.Request
import me.tatarka.inject.annotations.Inject

@Inject
class SeasonLastRequestStore(
dao: LastRequestDao,
) : EntityLastRequestStore(Request.SEASON_DETAILS, dao)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import app.tivi.data.compoundmodels.SeasonWithEpisodesAndWatches
import app.tivi.data.daos.EpisodesDao
import app.tivi.data.daos.SeasonsDao
import app.tivi.data.db.DatabaseTransactionRunner
import app.tivi.data.episodes.datasource.EpisodeWatchesDataSource
import app.tivi.data.models.ActionDate
import app.tivi.data.models.Episode
import app.tivi.data.models.EpisodeWatchEntry
Expand Down Expand Up @@ -37,13 +38,16 @@ class SeasonsEpisodesRepository(
private val episodeWatchStore: EpisodeWatchStore,
private val episodeWatchLastLastRequestStore: EpisodeWatchLastRequestStore,
private val episodeLastRequestStore: EpisodeLastRequestStore,
private val seasonLastRequestStore: SeasonLastRequestStore,
private val transactionRunner: DatabaseTransactionRunner,
private val seasonsDao: SeasonsDao,
private val episodesDao: EpisodesDao,
private val seasonsLastRequestStore: SeasonsLastRequestStore,
private val traktSeasonsDataSource: SeasonsEpisodesDataSource,
private val showSeasonsLastRequestStore: ShowSeasonsLastRequestStore,
private val tmdbSeasonsDataSource: TmdbSeasonsEpisodesDataSource,
private val traktSeasonsDataSource: TraktSeasonsEpisodesDataSource,
private val traktEpisodeDataSource: TraktEpisodeDataSource,
private val tmdbEpisodeDataSource: TmdbEpisodeDataSource,
private val traktEpisodeWatchesDataSource: EpisodeWatchesDataSource,
private val traktAuthRepository: TraktAuthRepository,
logger: Logger,
) {
Expand All @@ -69,10 +73,14 @@ class SeasonsEpisodesRepository(
return episodesDao.episodeWithIdObservable(episodeId).filterNotNull()
}

suspend fun getEpisode(episodeId: Long): Episode? {
fun getEpisode(episodeId: Long): Episode? {
return episodesDao.episodeWithId(episodeId)
}

fun getSeason(seasonId: Long): Season? {
return seasonsDao.seasonWithId(seasonId)
}

fun observeEpisodeWatches(episodeId: Long): Flow<List<EpisodeWatchEntry>> {
return episodeWatchStore.observeEpisodeWatches(episodeId)
}
Expand All @@ -85,7 +93,7 @@ class SeasonsEpisodesRepository(
suspend fun needShowSeasonsUpdate(
showId: Long,
expiry: Instant? = null,
): Boolean = seasonsLastRequestStore.isRequestBefore(
): Boolean = showSeasonsLastRequestStore.isRequestBefore(
entityId = showId,
instant = expiry ?: 7.days.inPast,
)
Expand All @@ -94,14 +102,25 @@ class SeasonsEpisodesRepository(
seasonsDao.deleteWithShowId(showId)
}

suspend fun updateSeasonsEpisodes(showId: Long) {
val response = traktSeasonsDataSource.getSeasonsEpisodes(showId)
response.distinctBy { it.first.number }.associate { (season, episodes) ->
val localSeason = seasonsDao.seasonWithTraktId(season.traktId!!)
suspend fun updateSeasonsEpisodes(showId: Long) = coroutineScope {
val traktDeferred = async { traktSeasonsDataSource.getSeasonsEpisodes(showId) }
val tmdbDeferred = async { tmdbSeasonsDataSource.getSeasonsEpisodes(showId) }

val trakt = traktDeferred.await().distinctBy { it.first.number }
val tmdb = tmdbDeferred.await().distinctBy { it.first.number }

trakt.associate { (traktSeason, traktEpisodes) ->
val localSeason = seasonsDao.seasonWithTraktId(traktSeason.traktId!!)
?: Season(showId = showId)
val mergedSeason = mergeSeason(localSeason, season, Season.EMPTY)

val mergedEpisodes = episodes.distinctBy(Episode::number).map {
val mergedSeason = mergeSeason(
local = localSeason,
trakt = traktSeason,
tmdb = tmdb.firstOrNull { it.first.number == traktSeason.number }?.first
?: Season.EMPTY,
)

val mergedEpisodes = traktEpisodes.distinctBy(Episode::number).map {
val localEpisode = episodesDao.episodeWithTraktId(it.traktId!!)
?: Episode(seasonId = mergedSeason.id)
mergeEpisode(localEpisode, it, Episode.EMPTY)
Expand All @@ -118,9 +137,9 @@ class SeasonsEpisodesRepository(
episodeSyncer.sync(episodesDao.episodesWithSeasonId(seasonId), updatedEpisodes)
}
}
}.also {
showSeasonsLastRequestStore.updateLastRequest(showId)
}

seasonsLastRequestStore.updateLastRequest(showId)
}

suspend fun needEpisodeUpdate(
Expand Down Expand Up @@ -165,6 +184,47 @@ class SeasonsEpisodesRepository(
episodeLastRequestStore.updateLastRequest(episodeId)
}

fun needSeasonUpdate(
seasonId: Long,
expiry: Instant = 28.days.inPast,
): Boolean {
return seasonLastRequestStore.isRequestBefore(seasonId, expiry)
}

suspend fun updateSeason(seasonId: Long) = coroutineScope {
val local = seasonsDao.seasonWithId(seasonId) ?: Season.EMPTY
val traktDeferred = async {
traktSeasonsDataSource.getSeason(local.showId, local.number!!)
}
val tmdbDeferred = async {
runCatching {
traktSeasonsDataSource.getSeason(local.showId, local.number!!)

This comment has been minimized.

Copy link
@MartelliEnrico

MartelliEnrico Oct 9, 2023

Shouldn't this be tmdbSeasonsDataSource?

This comment has been minimized.

Copy link
@chrisbanes

chrisbanes Oct 9, 2023

Author Owner

Woops, yes it should. Fixed in 1875905

}.getOrNull()
}

val trakt = try {
traktDeferred.await()
} catch (ce: CancellationException) {
throw ce
} catch (e: Exception) {
null
}
val tmdb = try {
tmdbDeferred.await()
} catch (ce: CancellationException) {
throw ce
} catch (e: Exception) {
null
}
check(trakt != null || tmdb != null)

seasonsDao.upsert(
mergeSeason(local, trakt ?: Season.EMPTY, tmdb ?: Season.EMPTY),
)

seasonLastRequestStore.updateLastRequest(seasonId)
}

suspend fun syncEpisodeWatchesForShow(showId: Long) {
// Process any pending deletes
episodeWatchStore.getEntriesWithDeleteAction(showId).also {
Expand Down Expand Up @@ -302,7 +362,7 @@ class SeasonsEpisodesRepository(
suspend fun updateShowEpisodeWatches(showId: Long) {
if (traktAuthRepository.state.value != TraktAuthState.LOGGED_IN) return

val response = traktSeasonsDataSource.getShowEpisodeWatches(showId)
val response = traktEpisodeWatchesDataSource.getShowEpisodeWatches(showId)

val watches = response.mapNotNull { (episode, watchEntry) ->
val epId = episodesDao.episodeIdWithTraktId(episode.traktId!!)
Expand All @@ -314,7 +374,7 @@ class SeasonsEpisodesRepository(
}

private suspend fun fetchEpisodeWatchesFromRemote(episodeId: Long) {
val response = traktSeasonsDataSource.getEpisodeWatches(episodeId, null)
val response = traktEpisodeWatchesDataSource.getEpisodeWatches(episodeId, null)
val watches = response.map { it.copy(episodeId = episodeId) }
episodeWatchStore.syncEpisodeWatchEntries(episodeId, watches)
}
Expand All @@ -334,7 +394,7 @@ class SeasonsEpisodesRepository(

if (entries.size > localOnlyDeletes.size) {
val toRemove = entries.filter { it.traktId != null }
traktSeasonsDataSource.removeEpisodeWatches(toRemove)
traktEpisodeWatchesDataSource.removeEpisodeWatches(toRemove)
// Now update the database
episodeWatchStore.deleteEntriesWithIds(entries.map(EpisodeWatchEntry::id))
return true
Expand All @@ -353,7 +413,7 @@ class SeasonsEpisodesRepository(
*/
private suspend fun processPendingAdditions(entries: List<EpisodeWatchEntry>): Boolean {
if (traktAuthRepository.state.value == TraktAuthState.LOGGED_IN) {
traktSeasonsDataSource.addEpisodeWatches(entries)
traktEpisodeWatchesDataSource.addEpisodeWatches(entries)
// Now update the database
episodeWatchStore.updateEntriesWithAction(entries.map { it.id }, PendingAction.NOTHING)
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import app.tivi.data.models.Request
import me.tatarka.inject.annotations.Inject

@Inject
class SeasonsLastRequestStore(
class ShowSeasonsLastRequestStore(
dao: LastRequestDao,
) : EntityLastRequestStore(Request.SHOW_SEASONS, dao)

This file was deleted.

Loading

0 comments on commit f07cf1e

Please sign in to comment.