diff --git a/data/src/main/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImpl.kt b/data/src/main/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImpl.kt index 8484e3d2..5e0e2c33 100644 --- a/data/src/main/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImpl.kt +++ b/data/src/main/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImpl.kt @@ -12,6 +12,7 @@ import com.miruplay.tv.model.Anime import com.miruplay.tv.model.DramaSeries import com.miruplay.tv.model.Episode import com.miruplay.tv.model.MetadataProviderRef +import com.miruplay.tv.model.distinctSeasonEpisodeCount import com.miruplay.tv.model.normalizedMetadataBinding import com.miruplay.tv.repository.dramaSeriesCacheKey import com.miruplay.tv.repository.toLegacyCachedDramaMetadata @@ -109,7 +110,7 @@ class MetadataRepositoryImpl @Inject constructor( // Also update episode count in cached anime metadata animeDao.getById(animeId)?.let { animeEntity -> animeDao.insert(animeEntity.copy( - episodeCount = episodes.size, + episodeCount = episodes.distinctSeasonEpisodeCount(), lastUpdated = System.currentTimeMillis() )) } @@ -205,7 +206,11 @@ private fun AnimeEntity.toDomain(episodeEntities: List): Anime { } ?: emptyList(), studio = studio, director = director, - episodeCount = episodeEntities.size.takeIf { it > 0 } ?: episodeCount, + episodeCount = episodeEntities + .map(EpisodeEntity::toDomain) + .distinctSeasonEpisodeCount() + .takeIf { it > 0 } + ?: episodeCount, airDate = airDate, rating = rating, bangumiId = bangumiId?.toIntOrNull(), diff --git a/data/src/test/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImplTest.kt b/data/src/test/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImplTest.kt index 5b7c0007..1149c9de 100644 --- a/data/src/test/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImplTest.kt +++ b/data/src/test/kotlin/com/miruplay/tv/data/repository/MetadataRepositoryImplTest.kt @@ -300,6 +300,24 @@ class MetadataRepositoryImplTest { assertEquals(1, cached.first { it.id == "anime-b" }.episodeCount) } + @Test + fun `getCachedMetadata collection should count logical episodes instead of files`() = runBlocking { + repository.cacheMetadata(createTestAnime(id = "anime-a", title = "Anime A")) + val episodes = listOf( + createTestEpisode(id = "ep-1-web", animeId = "anime-a", episodeNumber = 1), + createTestEpisode(id = "ep-1-bd", animeId = "anime-a", episodeNumber = 1), + createTestEpisode(id = "ep-2", animeId = "anime-a", episodeNumber = 2), + createTestEpisode(id = "s2-ep-1", animeId = "anime-a", seasonNumber = 2, episodeNumber = 1), + ) + + repository.cacheEpisodes("anime-a", episodes) + val cached = repository.getCachedMetadata(listOf("anime-a")).getOrNull()!!.single() + + assertEquals(3, cached.episodeCount) + assertEquals(3, animeDao.getById("anime-a")?.episodeCount) + assertEquals(4, repository.getCachedEpisodes("anime-a").getOrNull()!!.size) + } + @Test fun `getCachedMetadata collection should keep stale anime`() = runBlocking { repository.cacheMetadata(createTestAnime(id = "fresh-anime")) diff --git a/repository-api/src/main/kotlin/com/miruplay/tv/repository/MediaIndexPosterGrouping.kt b/repository-api/src/main/kotlin/com/miruplay/tv/repository/MediaIndexPosterGrouping.kt index a1297064..eb0bd0c4 100644 --- a/repository-api/src/main/kotlin/com/miruplay/tv/repository/MediaIndexPosterGrouping.kt +++ b/repository-api/src/main/kotlin/com/miruplay/tv/repository/MediaIndexPosterGrouping.kt @@ -10,6 +10,15 @@ data class MediaIndexPosterGroup( val entries: List, val animeId: String = entries.posterGroupAnimeId(mergeSameAnimeEnabled = false), ) { + val episodeCount: Int = entries + .asSequence() + .filterNot(MediaIndexEntry::isSeriesExtra) + .distinctBy { entry -> + entry.episodeNumber?.let { episodeNumber -> + (entry.seasonNumber ?: 1) to episodeNumber + } ?: entry.path + } + .count() val primaryEntry: MediaIndexEntry = entries .filterNot(MediaIndexEntry::isSeriesExtra) @@ -24,7 +33,6 @@ data class MediaIndexPosterGroup( ?: entry.animeName?.takeIf { it.isNotBlank() }?.let { "title:${it.lowercase()}" } } val subtitle: String = buildString { - val episodeCount = entries.count { !it.isSeriesExtra() } append(episodeCount) append(" episode") if (episodeCount != 1) append('s') @@ -65,7 +73,7 @@ fun MediaIndexPosterGroup.toIndexedAnime(): Anime = Anime( id = animeId, title = title, - episodeCount = entries.count { !it.isSeriesExtra() }, + episodeCount = episodeCount, summary = primaryEntry.plot.orEmpty(), ) diff --git a/repository-api/src/test/kotlin/com/miruplay/tv/repository/LibraryAnimeResolverTest.kt b/repository-api/src/test/kotlin/com/miruplay/tv/repository/LibraryAnimeResolverTest.kt index 75e14fc6..a037e622 100644 --- a/repository-api/src/test/kotlin/com/miruplay/tv/repository/LibraryAnimeResolverTest.kt +++ b/repository-api/src/test/kotlin/com/miruplay/tv/repository/LibraryAnimeResolverTest.kt @@ -156,6 +156,7 @@ class LibraryAnimeResolverTest { sourceId = 1L, path = "D:/Anime/Show/S1E01.mkv", animeName = "Show Season 1", + seasonNumber = 1, episodeNumber = 1, metadataId = "bgm-1", metadataTitle = "Shared Show", @@ -164,6 +165,7 @@ class LibraryAnimeResolverTest { sourceId = 1L, path = "D:/Anime/Show/S2E01.mkv", animeName = "Show Season 2", + seasonNumber = 2, episodeNumber = 1, metadataId = "bgm-1", metadataTitle = "Shared Show", diff --git a/repository-api/src/test/kotlin/com/miruplay/tv/repository/MediaIndexPosterGroupingTest.kt b/repository-api/src/test/kotlin/com/miruplay/tv/repository/MediaIndexPosterGroupingTest.kt index 598cb0db..8385cbef 100644 --- a/repository-api/src/test/kotlin/com/miruplay/tv/repository/MediaIndexPosterGroupingTest.kt +++ b/repository-api/src/test/kotlin/com/miruplay/tv/repository/MediaIndexPosterGroupingTest.kt @@ -49,6 +49,20 @@ class MediaIndexPosterGroupingTest { assertEquals(0, extrasOnly.toIndexedAnime().episodeCount) } + @Test + fun `poster episode count groups multiple files for the same episode`() { + val group = listOf( + MediaIndexEntry(sourceId = 1, path = "show/WEB/S01E01.mkv", animeName = "Show", seasonNumber = 1, episodeNumber = 1), + MediaIndexEntry(sourceId = 1, path = "show/BD/S01E01.mkv", animeName = "Show", seasonNumber = 1, episodeNumber = 1), + MediaIndexEntry(sourceId = 1, path = "show/WEB/S01E02.mkv", animeName = "Show", seasonNumber = 1, episodeNumber = 2), + MediaIndexEntry(sourceId = 1, path = "show/WEB/S02E01.mkv", animeName = "Show", seasonNumber = 2, episodeNumber = 1), + ).toMediaIndexPosterGroups().single() + + assertEquals("3 episodes ยท S1", group.subtitle) + assertEquals(3, group.toIndexedAnime().episodeCount) + assertEquals(4, group.entries.size) + } + @Test fun `poster groups can merge entries that share external metadata`() { val entries = listOf(