feat(collections): add TMDB Discover exclusion filters

This commit is contained in:
tapframe 2026-08-09 21:50:17 +05:30
parent ee40c8c422
commit 0fc4616b82
5 changed files with 125 additions and 4 deletions

View file

@ -177,6 +177,9 @@
<string name="collections_editor_tmdb_genres_helper">Use TMDB genre numbers. Separate multiple with commas for AND, or pipes for OR.</string>
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
<string name="collections_editor_tmdb_without_genres">Excluded genre IDs</string>
<string name="collections_editor_tmdb_without_genres_helper">Exclude titles matching these TMDB genre numbers.</string>
<string name="collections_editor_tmdb_without_genres_placeholder">16 for Animation</string>
<string name="collections_editor_tmdb_date_from">Release or air date from</string>
<string name="collections_editor_tmdb_date_to">Release or air date to</string>
<string name="collections_editor_tmdb_date_helper">Use YYYY-MM-DD, for example 2024-01-01.</string>
@ -199,9 +202,15 @@
<string name="collections_editor_tmdb_keywords">Keyword IDs</string>
<string name="collections_editor_tmdb_keywords_helper">Use TMDB keyword numbers. Quick chips fill common examples.</string>
<string name="collections_editor_tmdb_keywords_placeholder">9715 for superhero</string>
<string name="collections_editor_tmdb_without_keywords">Excluded keyword IDs</string>
<string name="collections_editor_tmdb_without_keywords_helper">Exclude titles matching these TMDB keyword numbers.</string>
<string name="collections_editor_tmdb_without_keywords_placeholder">9715</string>
<string name="collections_editor_tmdb_companies">Company IDs</string>
<string name="collections_editor_tmdb_companies_helper">Use studio/company IDs. Quick chips fill common examples.</string>
<string name="collections_editor_tmdb_companies_placeholder">420 for Marvel Studios</string>
<string name="collections_editor_tmdb_without_companies">Excluded company IDs</string>
<string name="collections_editor_tmdb_without_companies_helper">Exclude titles associated with these TMDB company numbers.</string>
<string name="collections_editor_tmdb_without_companies_placeholder">420</string>
<string name="collections_editor_tmdb_networks">Network IDs</string>
<string name="collections_editor_tmdb_networks_helper">For series only. Use network IDs like Netflix 213 or HBO 49.</string>
<string name="collections_editor_tmdb_networks_placeholder">213 for Netflix</string>
@ -282,6 +291,9 @@
<string name="collections_editor_tmdb_watch_providers">Watch provider IDs</string>
<string name="collections_editor_tmdb_watch_providers_helper">Use TMDB watch provider IDs. Separate multiple with commas for AND, or pipes for OR.</string>
<string name="collections_editor_tmdb_watch_providers_placeholder">8|337|350</string>
<string name="collections_editor_tmdb_without_watch_providers">Excluded watch provider IDs</string>
<string name="collections_editor_tmdb_without_watch_providers_helper">Exclude titles available from these TMDB watch provider numbers in the selected region.</string>
<string name="collections_editor_tmdb_without_watch_providers_placeholder">8|337|350</string>
<string name="collections_editor_tmdb_quick_watch_providers">Quick watch providers</string>
<string name="collections_editor_tmdb_watch_provider_netflix">Netflix</string>
<string name="collections_editor_tmdb_watch_provider_prime">Prime Video</string>

View file

@ -1280,6 +1280,17 @@ private fun TmdbSourcePickerScreen(
}
},
)
TmdbFilterField(
label = stringResource(Res.string.collections_editor_tmdb_without_genres),
helper = stringResource(Res.string.collections_editor_tmdb_without_genres_helper),
value = state.tmdbFilters.withoutGenres.orEmpty(),
placeholder = stringResource(Res.string.collections_editor_tmdb_without_genres_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withoutGenres = value.ifBlank { null })
}
},
)
TmdbFilterField(
label = stringResource(Res.string.collections_editor_tmdb_date_from),
helper = stringResource(Res.string.collections_editor_tmdb_date_helper),
@ -1406,6 +1417,17 @@ private fun TmdbSourcePickerScreen(
}
},
)
TmdbFilterField(
label = stringResource(Res.string.collections_editor_tmdb_without_keywords),
helper = stringResource(Res.string.collections_editor_tmdb_without_keywords_helper),
value = state.tmdbFilters.withoutKeywords.orEmpty(),
placeholder = stringResource(Res.string.collections_editor_tmdb_without_keywords_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withoutKeywords = value.ifBlank { null })
}
},
)
TmdbQuickChips(
label = stringResource(Res.string.collections_editor_tmdb_quick_studios),
chips = listOf(
@ -1430,6 +1452,17 @@ private fun TmdbSourcePickerScreen(
}
},
)
TmdbFilterField(
label = stringResource(Res.string.collections_editor_tmdb_without_companies),
helper = stringResource(Res.string.collections_editor_tmdb_without_companies_helper),
value = state.tmdbFilters.withoutCompanies.orEmpty(),
placeholder = stringResource(Res.string.collections_editor_tmdb_without_companies_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withoutCompanies = value.ifBlank { null })
}
},
)
TmdbQuickChips(
label = stringResource(Res.string.collections_editor_tmdb_quick_networks),
chips = listOf(
@ -1489,6 +1522,17 @@ private fun TmdbSourcePickerScreen(
}
},
)
TmdbFilterField(
label = stringResource(Res.string.collections_editor_tmdb_without_watch_providers),
helper = stringResource(Res.string.collections_editor_tmdb_without_watch_providers_helper),
value = state.tmdbFilters.withoutWatchProviders.orEmpty(),
placeholder = stringResource(Res.string.collections_editor_tmdb_without_watch_providers_placeholder),
onValueChange = { value ->
CollectionEditorRepository.updateTmdbFilters {
it.copy(withoutWatchProviders = value.ifBlank { null })
}
},
)
TmdbQuickChips(
label = stringResource(Res.string.collections_editor_tmdb_quick_watch_regions),
chips = listOf(

View file

@ -154,6 +154,7 @@ enum class TraktSortHow(val value: String) {
@Serializable
data class TmdbCollectionFilters(
val withGenres: String? = null,
val withoutGenres: String? = null,
val releaseDateGte: String? = null,
val releaseDateLte: String? = null,
val voteAverageGte: Double? = null,
@ -162,11 +163,14 @@ data class TmdbCollectionFilters(
val withOriginalLanguage: String? = null,
val withOriginCountry: String? = null,
val withKeywords: String? = null,
val withoutKeywords: String? = null,
val withCompanies: String? = null,
val withoutCompanies: String? = null,
val withNetworks: String? = null,
val year: Int? = null,
val watchRegion: String? = null,
val withWatchProviders: String? = null,
val withoutWatchProviders: String? = null,
)
data class TmdbSourceImportMetadata(

View file

@ -312,7 +312,7 @@ object TmdbCollectionSourceResolver {
)
}
private fun buildDiscoverQuery(
internal fun buildDiscoverQuery(
source: CollectionSource,
sourceType: TmdbCollectionSourceType,
mediaType: TmdbCollectionMediaType,
@ -331,18 +331,26 @@ object TmdbCollectionSourceResolver {
val companyId = source.tmdbId?.toString().takeIf { sourceType == TmdbCollectionSourceType.COMPANY }
val networkId = source.tmdbId?.toString().takeIf { sourceType == TmdbCollectionSourceType.NETWORK }
putIfNotBlank("with_companies", companyId ?: filters.withCompanies)
putIfNotBlank("without_companies", filters.withoutCompanies)
putIfNotBlank("with_networks", networkId ?: filters.withNetworks)
putIfNotBlank("with_genres", filters.withGenres)
putIfNotBlank("without_genres", filters.withoutGenres)
putIfNotBlank("vote_count.gte", filters.voteCountGte?.toString())
putIfNotBlank("vote_average.gte", filters.voteAverageGte?.toString())
putIfNotBlank("vote_average.lte", filters.voteAverageLte?.toString())
putIfNotBlank("with_original_language", filters.withOriginalLanguage)
putIfNotBlank("with_origin_country", filters.withOriginCountry)
putIfNotBlank("with_keywords", filters.withKeywords)
if (!filters.withWatchProviders.isNullOrBlank()) {
put("with_watch_providers", filters.withWatchProviders)
putIfNotBlank("without_keywords", filters.withoutKeywords)
val withWatchProviders = filters.withWatchProviders?.takeIf { it.isNotBlank() }
val withoutWatchProviders = filters.withoutWatchProviders?.takeIf { it.isNotBlank() }
if (withWatchProviders != null || withoutWatchProviders != null) {
putIfNotBlank("with_watch_providers", withWatchProviders)
putIfNotBlank("without_watch_providers", withoutWatchProviders)
put("watch_region", filters.watchRegion?.takeIf { it.isNotBlank() } ?: "US")
put("with_watch_monetization_types", "flatrate|free|ads|rent|buy")
if (withWatchProviders != null) {
put("with_watch_monetization_types", "flatrate|free|ads|rent|buy")
}
}
putIfNotBlank("year", filters.year?.takeIf { mediaType == TmdbCollectionMediaType.MOVIE }?.toString())
putIfNotBlank("first_air_date_year", filters.year?.takeIf { mediaType == TmdbCollectionMediaType.TV }?.toString())

View file

@ -57,6 +57,59 @@ class CollectionSourceSerializationTest {
assertEquals(TraktSortHow.DESC.value, source.sortHow)
}
@Test
fun tmdbExclusionFiltersRoundTripAndBuildDiscoverQuery() {
val filters = TmdbCollectionFilters(
withoutGenres = "16",
withoutKeywords = "9715|818",
withoutCompanies = "420",
withoutWatchProviders = "8|337",
)
val collection = Collection(
id = "collection-1",
title = "Live Action",
folders = listOf(
CollectionFolder(
id = "folder-1",
title = "Movies",
sources = listOf(
CollectionSource(
provider = "tmdb",
tmdbSourceType = TmdbCollectionSourceType.DISCOVER.name,
title = "Without Animation",
mediaType = TmdbCollectionMediaType.MOVIE.name,
filters = filters,
),
),
),
),
)
val encoded = json.encodeToString(listOf(collection))
val decodedSource = json.decodeFromString<List<Collection>>(encoded)
.single()
.folders
.single()
.resolvedSources
.single()
assertEquals(filters, decodedSource.filters)
val query = TmdbCollectionSourceResolver.buildDiscoverQuery(
source = decodedSource,
sourceType = TmdbCollectionSourceType.DISCOVER,
mediaType = TmdbCollectionMediaType.MOVIE,
language = "en-US",
page = 1,
filters = decodedSource.filters ?: TmdbCollectionFilters(),
)
assertEquals("16", query["without_genres"])
assertEquals("9715|818", query["without_keywords"])
assertEquals("420", query["without_companies"])
assertEquals("8|337", query["without_watch_providers"])
assertEquals("US", query["watch_region"])
assertFalse("with_watch_monetization_types" in query)
}
@Test
fun importedTraktSourceWithoutListIdIsRejected() {
val payload = """