diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index e4f9ff23..4a014ff0 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -110,29 +110,38 @@ Production Network Collection + Person + Director Custom Pick a ready-made source. You can edit or remove it after adding. Paste a public TMDB list URL or only the number from the URL. Search by studio name, or paste a TMDB company ID/URL and add it directly. Enter a network ID. Common networks are available in Presets and quick filters. Search a movie collection name or paste the collection ID from TMDB. + Enter a TMDB person ID or URL to build a row from cast credits. + Enter a TMDB person ID or URL to build a row from director credits. Build a live TMDB row using optional filters. Leave fields empty when you do not need that filter. Public TMDB list Network ID Collection ID + Person ID Production company name, ID, or URL TMDB ID or URL https://www.themoviedb.org/list/8504994 or 8504994 213 for Netflix, 49 for HBO, 2739 for Disney+ 10 for Star Wars Collection Marvel Studios, 420, or company URL + 31 for Tom Hanks, or person URL Examples: Marvel Studios, 420, or https://www.themoviedb.org/company/420. Example: Star Wars Collection, Harry Potter Collection, or a collection URL. Example IDs: Netflix 213, HBO 49, Disney+ 2739. Example: https://www.themoviedb.org/list/8504994 or 8504994. + Example: https://www.themoviedb.org/person/31-tom-hanks or 31. Display title Shown as the row/tab name. If blank, Nuvio creates one from the source. Marvel Movies, Netflix Originals, Pixar + Tom Hanks Movies, Favorite Actors + Christopher Nolan Movies, Favorite Directors Best Action Movies, Korean Dramas, 2024 Animation Search Results TMDB Collection @@ -212,6 +221,7 @@ Disney+ Prime Video Hulu + Original Popular Top Rated Recent @@ -219,6 +229,8 @@ TMDB Movie Collection Production Network + Person + Director TMDB Discover Create one to organize your catalogs. No collections yet diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt index cbb476c8..f7597072 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorRepository.kt @@ -46,6 +46,8 @@ enum class TmdbBuilderMode { PRODUCTION, NETWORK, COLLECTION, + PERSON, + DIRECTOR, DISCOVER, } @@ -340,9 +342,15 @@ object CollectionEditorRepository { } else { _uiState.value.tmdbMediaType } + val sortBy = when (mode) { + TmdbBuilderMode.LIST, + TmdbBuilderMode.COLLECTION -> TmdbCollectionSort.ORIGINAL.value + else -> TmdbCollectionSort.POPULAR_DESC.value + } _uiState.value = _uiState.value.copy( tmdbBuilderMode = mode, tmdbMediaType = mediaType, + tmdbSortBy = sortBy, tmdbMediaBoth = if ( mode == TmdbBuilderMode.NETWORK || mode == TmdbBuilderMode.LIST || @@ -459,6 +467,8 @@ object CollectionEditorRepository { TmdbBuilderMode.COLLECTION -> TmdbCollectionSourceType.COLLECTION TmdbBuilderMode.PRODUCTION -> TmdbCollectionSourceType.COMPANY TmdbBuilderMode.NETWORK -> TmdbCollectionSourceType.NETWORK + TmdbBuilderMode.PERSON -> TmdbCollectionSourceType.PERSON + TmdbBuilderMode.DIRECTOR -> TmdbCollectionSourceType.DIRECTOR TmdbBuilderMode.DISCOVER -> TmdbCollectionSourceType.DISCOVER } val id = TmdbCollectionSourceResolver.parseTmdbId(state.tmdbInput) @@ -473,6 +483,8 @@ object CollectionEditorRepository { TmdbCollectionSourceType.COLLECTION -> "TMDB Collection ${id ?: ""}".trim() TmdbCollectionSourceType.COMPANY -> "TMDB Production ${id ?: ""}".trim() TmdbCollectionSourceType.NETWORK -> "TMDB Network ${id ?: ""}".trim() + TmdbCollectionSourceType.PERSON -> "TMDB Person ${id ?: ""}".trim() + TmdbCollectionSourceType.DIRECTOR -> "TMDB Director ${id ?: ""}".trim() TmdbCollectionSourceType.DISCOVER -> "TMDB Discover" } } @@ -561,6 +573,8 @@ private val coverMetadataSourceTypes = setOf( TmdbCollectionSourceType.COLLECTION, TmdbCollectionSourceType.COMPANY, TmdbCollectionSourceType.NETWORK, + TmdbCollectionSourceType.PERSON, + TmdbCollectionSourceType.DIRECTOR, ) private fun CollectionCatalogSource.toCollectionSource(): CollectionSource = @@ -591,6 +605,8 @@ private fun selectedMediaTypes( ): List = when (sourceType) { TmdbCollectionSourceType.COMPANY, + TmdbCollectionSourceType.PERSON, + TmdbCollectionSourceType.DIRECTOR, TmdbCollectionSourceType.DISCOVER -> if (state.tmdbMediaBoth) { listOf(TmdbCollectionMediaType.MOVIE, TmdbCollectionMediaType.TV) } else { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index 41ee6532..1a7bdf04 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt @@ -897,13 +897,19 @@ private fun TmdbSourcePickerScreen( TmdbBuilderMode.COLLECTION -> TmdbCollectionSourceType.COLLECTION TmdbBuilderMode.PRODUCTION -> TmdbCollectionSourceType.COMPANY TmdbBuilderMode.NETWORK -> TmdbCollectionSourceType.NETWORK + TmdbBuilderMode.PERSON -> TmdbCollectionSourceType.PERSON + TmdbBuilderMode.DIRECTOR -> TmdbCollectionSourceType.DIRECTOR TmdbBuilderMode.DISCOVER -> TmdbCollectionSourceType.DISCOVER } val requiresId = sourceType != TmdbCollectionSourceType.DISCOVER val showMediaControls = state.tmdbBuilderMode == TmdbBuilderMode.PRODUCTION || + state.tmdbBuilderMode == TmdbBuilderMode.PERSON || + state.tmdbBuilderMode == TmdbBuilderMode.DIRECTOR || state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER val showSortControls = state.tmdbBuilderMode == TmdbBuilderMode.PRODUCTION || state.tmdbBuilderMode == TmdbBuilderMode.NETWORK || + state.tmdbBuilderMode == TmdbBuilderMode.PERSON || + state.tmdbBuilderMode == TmdbBuilderMode.DIRECTOR || state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER val showFilterControls = state.tmdbBuilderMode == TmdbBuilderMode.DISCOVER @@ -1892,6 +1898,8 @@ private fun tmdbBuilderModeLabel(mode: TmdbBuilderMode): String = TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_production_mode) TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_mode) TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_mode) + TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_person_mode) + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_director_mode) TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_custom_mode) } @@ -1903,6 +1911,8 @@ private fun tmdbModeHelpText(mode: TmdbBuilderMode): String = TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_help_production) TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_help_network) TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_help_collection) + TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_help_person) + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_help_director) TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_help_discover) } @@ -1913,6 +1923,8 @@ private fun tmdbInputLabel(mode: TmdbBuilderMode): String = TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_id) TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_id) TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_company_search) + TmdbBuilderMode.PERSON, + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_id) else -> stringResource(Res.string.collections_editor_tmdb_id_or_url) } @@ -1923,6 +1935,8 @@ private fun tmdbInputPlaceholder(mode: TmdbBuilderMode): String = TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_placeholder) TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_placeholder) TmdbBuilderMode.PRODUCTION -> stringResource(Res.string.collections_editor_tmdb_company_placeholder) + TmdbBuilderMode.PERSON, + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_placeholder) else -> stringResource(Res.string.collections_editor_tmdb_id_or_url) } @@ -1933,6 +1947,8 @@ private fun tmdbInputHelper(mode: TmdbBuilderMode): String = TmdbBuilderMode.COLLECTION -> stringResource(Res.string.collections_editor_tmdb_collection_helper) TmdbBuilderMode.NETWORK -> stringResource(Res.string.collections_editor_tmdb_network_helper) TmdbBuilderMode.LIST -> stringResource(Res.string.collections_editor_tmdb_list_helper) + TmdbBuilderMode.PERSON, + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_person_helper) else -> "" } @@ -1940,12 +1956,15 @@ private fun tmdbInputHelper(mode: TmdbBuilderMode): String = private fun tmdbTitlePlaceholder(mode: TmdbBuilderMode): String = when (mode) { TmdbBuilderMode.DISCOVER -> stringResource(Res.string.collections_editor_tmdb_discover_title_placeholder) + TmdbBuilderMode.PERSON -> stringResource(Res.string.collections_editor_tmdb_person_title_placeholder) + TmdbBuilderMode.DIRECTOR -> stringResource(Res.string.collections_editor_tmdb_director_title_placeholder) else -> stringResource(Res.string.collections_editor_tmdb_title_placeholder) } @Composable private fun tmdbSortLabel(sort: TmdbCollectionSort): String = when (sort) { + TmdbCollectionSort.ORIGINAL -> stringResource(Res.string.collections_editor_tmdb_sort_original) TmdbCollectionSort.POPULAR_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_popular) TmdbCollectionSort.VOTE_AVERAGE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_top_rated) TmdbCollectionSort.RELEASE_DATE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_recent) @@ -1979,6 +1998,16 @@ private fun tmdbSourceSubtitle(source: CollectionSource): String { stringResource(Res.string.collections_editor_tmdb_series), sort, ).joinToString(" • ") + TmdbCollectionSourceType.PERSON -> listOf( + stringResource(Res.string.collections_editor_tmdb_subtitle_person), + media, + sort, + ).joinToString(" • ") + TmdbCollectionSourceType.DIRECTOR -> listOf( + stringResource(Res.string.collections_editor_tmdb_subtitle_director), + media, + sort, + ).joinToString(" • ") TmdbCollectionSourceType.DISCOVER -> listOf( stringResource(Res.string.collections_editor_tmdb_subtitle_discover), media, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt index 208aa03d..f0780ad2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt @@ -69,6 +69,8 @@ enum class TmdbCollectionSourceType { COMPANY, NETWORK, DISCOVER, + PERSON, + DIRECTOR, } @Serializable @@ -86,6 +88,7 @@ enum class TmdbCollectionMediaType(val value: String) { } enum class TmdbCollectionSort(val value: String) { + ORIGINAL("original"), POPULAR_DESC("popularity.desc"), VOTE_AVERAGE_DESC("vote_average.desc"), RELEASE_DATE_DESC("primary_release_date.desc"), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt index e853eeba..decff7a5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/FolderDetailRepository.kt @@ -140,7 +140,11 @@ object FolderDetailRepository { source = source, type = type, catalogId = tmdbCatalogId(source), - supportsPagination = source.tmdbSourceType != TmdbCollectionSourceType.COLLECTION.name, + supportsPagination = source.tmdbSourceType !in setOf( + TmdbCollectionSourceType.COLLECTION.name, + TmdbCollectionSourceType.PERSON.name, + TmdbCollectionSourceType.DIRECTOR.name, + ), isLoading = true, ), ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt index ee25fa48..3f37d3d8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt @@ -29,6 +29,8 @@ object TmdbCollectionSourceResolver { when (sourceType) { TmdbCollectionSourceType.LIST -> resolveList(source, apiKey, language, page) TmdbCollectionSourceType.COLLECTION -> resolveCollection(source, apiKey, language) + TmdbCollectionSourceType.PERSON, + TmdbCollectionSourceType.DIRECTOR -> resolvePersonCredits(source, apiKey, language) TmdbCollectionSourceType.COMPANY, TmdbCollectionSourceType.NETWORK, TmdbCollectionSourceType.DISCOVER -> resolveDiscover(source, apiKey, language, page) @@ -85,6 +87,19 @@ object TmdbCollectionSourceResolver { ) } + TmdbCollectionSourceType.PERSON, + TmdbCollectionSourceType.DIRECTOR -> { + val body = fetch( + endpoint = "person/$id", + apiKey = apiKey, + query = mapOf("language" to language), + ) ?: error("TMDB person not found") + TmdbSourceImportMetadata( + title = body.name?.takeIf { it.isNotBlank() }, + coverImageUrl = imageUrl(body.profilePath, "w500"), + ) + } + TmdbCollectionSourceType.DISCOVER -> TmdbSourceImportMetadata(title = "TMDB Discover") } } @@ -153,7 +168,7 @@ object TmdbCollectionSourceResolver { fun parseTmdbId(input: String): Int? { val trimmed = input.trim() trimmed.toIntOrNull()?.let { return it } - return Regex("""(?:list|collection|company|network)/(\d+)""") + return Regex("""(?:list|collection|company|network|person)/(\d+)""") .find(trimmed) ?.groupValues ?.getOrNull(1) @@ -193,6 +208,7 @@ object TmdbCollectionSourceResolver { ) ?: error("TMDB list not found") val items = body.items.orEmpty() .mapNotNull { it.toPreview() } + .sortedFor(source.sortBy) .distinctBy { "${it.type}:${it.id}" } return CatalogPage( items = items, @@ -213,12 +229,35 @@ object TmdbCollectionSourceResolver { query = mapOf("language" to language), ) ?: error("TMDB collection not found") val items = body.parts.orEmpty() - .sortedBy { it.releaseDate ?: "9999" } .mapNotNull { it.toPreview(TmdbCollectionMediaType.MOVIE) } + .sortedFor(source.sortBy) .distinctBy { it.id } return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null) } + private suspend fun resolvePersonCredits( + source: CollectionSource, + apiKey: String, + language: String, + ): CatalogPage { + val id = source.tmdbId ?: error("Missing TMDB person ID") + val mediaType = source.tmdbMediaType() + val body = fetch( + endpoint = "person/$id/combined_credits", + apiKey = apiKey, + query = mapOf("language" to language), + ) ?: error("TMDB person credits not found") + val items = when (source.tmdbType()) { + TmdbCollectionSourceType.DIRECTOR -> body.crew.orEmpty() + .filter { it.job.equals("Director", ignoreCase = true) } + .mapNotNull { it.toPreview(mediaType) } + else -> body.cast.orEmpty().mapNotNull { it.toPreview(mediaType) } + } + .distinctBy { "${it.type}:${it.id}" } + .sortedFor(source.sortBy) + return CatalogPage(items = items, rawItemCount = items.size, nextSkip = null) + } + private suspend fun resolveDiscover( source: CollectionSource, apiKey: String, @@ -312,6 +351,21 @@ object TmdbCollectionSourceResolver { }.getOrNull() } + private fun List.sortedFor(sortBy: String?): List = + when (sortBy) { + TmdbCollectionSort.ORIGINAL.value -> this + TmdbCollectionSort.VOTE_AVERAGE_DESC.value -> sortedWith( + compareByDescending { it.imdbRating?.toDoubleOrNull() ?: -1.0 } + .thenByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() }, + ) + TmdbCollectionSort.RELEASE_DATE_DESC.value, + TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> sortedByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() } + TmdbCollectionSort.POPULAR_DESC.value, + null, + "" -> this + else -> this + } + private fun TmdbListItem.toPreview(): MetaPreview? { val media = mediaType?.lowercase() val contentType = if (media == "tv") TmdbCollectionMediaType.TV else TmdbCollectionMediaType.MOVIE @@ -362,6 +416,62 @@ object TmdbCollectionSourceResolver { ) } + private fun TmdbPersonCreditCast.toPreview(mediaType: TmdbCollectionMediaType): MetaPreview? { + if (!matchesMediaType(mediaType, this.mediaType)) return null + val title = title?.takeIf { it.isNotBlank() } + ?: name?.takeIf { it.isNotBlank() } + ?: originalTitle?.takeIf { it.isNotBlank() } + ?: originalName?.takeIf { it.isNotBlank() } + ?: return null + return MetaPreview( + id = "tmdb:$id", + type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie", + name = title, + poster = imageUrl(posterPath, "w500") ?: imageUrl(backdropPath, "w780"), + banner = imageUrl(backdropPath, "w1280"), + posterShape = PosterShape.Poster, + description = overview?.takeIf { it.isNotBlank() }, + releaseInfo = when (mediaType) { + TmdbCollectionMediaType.MOVIE -> releaseDate?.take(4) + TmdbCollectionMediaType.TV -> firstAirDate?.take(4) + }, + rawReleaseDate = when (mediaType) { + TmdbCollectionMediaType.MOVIE -> releaseDate + TmdbCollectionMediaType.TV -> firstAirDate + }, + popularity = popularity, + imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, + ) + } + + private fun TmdbPersonCreditCrew.toPreview(mediaType: TmdbCollectionMediaType): MetaPreview? { + if (!matchesMediaType(mediaType, this.mediaType)) return null + val title = title?.takeIf { it.isNotBlank() } + ?: name?.takeIf { it.isNotBlank() } + ?: originalTitle?.takeIf { it.isNotBlank() } + ?: originalName?.takeIf { it.isNotBlank() } + ?: return null + return MetaPreview( + id = "tmdb:$id", + type = if (mediaType == TmdbCollectionMediaType.TV) "series" else "movie", + name = title, + poster = imageUrl(posterPath, "w500") ?: imageUrl(backdropPath, "w780"), + banner = imageUrl(backdropPath, "w1280"), + posterShape = PosterShape.Poster, + description = overview?.takeIf { it.isNotBlank() }, + releaseInfo = when (mediaType) { + TmdbCollectionMediaType.MOVIE -> releaseDate?.take(4) + TmdbCollectionMediaType.TV -> firstAirDate?.take(4) + }, + rawReleaseDate = when (mediaType) { + TmdbCollectionMediaType.MOVIE -> releaseDate + TmdbCollectionMediaType.TV -> firstAirDate + }, + popularity = popularity, + imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, + ) + } + private fun CollectionSource.tmdbType(): TmdbCollectionSourceType = tmdbSourceType ?.let { raw -> runCatching { TmdbCollectionSourceType.valueOf(raw.uppercase()) }.getOrNull() } @@ -370,6 +480,12 @@ object TmdbCollectionSourceResolver { private fun CollectionSource.tmdbMediaType(): TmdbCollectionMediaType = TmdbCollectionMediaType.fromString(mediaType) + private fun matchesMediaType(expected: TmdbCollectionMediaType, actual: String?): Boolean = + when (expected) { + TmdbCollectionMediaType.MOVIE -> actual == "movie" + TmdbCollectionMediaType.TV -> actual == "tv" + } + private fun company(title: String, id: Int) = CollectionSource( provider = "tmdb", tmdbSourceType = TmdbCollectionSourceType.COMPANY.name, @@ -391,6 +507,7 @@ object TmdbCollectionSourceResolver { private fun movieSort(sortBy: String?): String = when (sortBy) { TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> TmdbCollectionSort.RELEASE_DATE_DESC.value + TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value null, "" -> TmdbCollectionSort.POPULAR_DESC.value else -> sortBy } @@ -398,6 +515,7 @@ object TmdbCollectionSourceResolver { private fun tvSort(sortBy: String?): String = when (sortBy) { TmdbCollectionSort.RELEASE_DATE_DESC.value -> TmdbCollectionSort.FIRST_AIR_DATE_DESC.value + TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value null, "" -> TmdbCollectionSort.POPULAR_DESC.value else -> sortBy } @@ -449,6 +567,12 @@ private data class TmdbNetworkResponse( @SerialName("logo_path") val logoPath: String? = null, ) +@Serializable +private data class TmdbPersonResponse( + val name: String? = null, + @SerialName("profile_path") val profilePath: String? = null, +) + @Serializable data class TmdbCompanySearchResult( val id: Int, @@ -496,6 +620,47 @@ private data class TmdbGenreItem( val name: String, ) +@Serializable +private data class TmdbPersonCreditsResponse( + val cast: List? = null, + val crew: List? = null, +) + +@Serializable +private data class TmdbPersonCreditCast( + val id: Int, + @SerialName("media_type") val mediaType: String? = null, + val title: String? = null, + val name: String? = null, + @SerialName("original_title") val originalTitle: String? = null, + @SerialName("original_name") val originalName: String? = null, + val overview: String? = null, + @SerialName("poster_path") val posterPath: String? = null, + @SerialName("backdrop_path") val backdropPath: String? = null, + @SerialName("release_date") val releaseDate: String? = null, + @SerialName("first_air_date") val firstAirDate: String? = null, + @SerialName("vote_average") val voteAverage: Double? = null, + val popularity: Double? = null, +) + +@Serializable +private data class TmdbPersonCreditCrew( + val id: Int, + @SerialName("media_type") val mediaType: String? = null, + val title: String? = null, + val name: String? = null, + @SerialName("original_title") val originalTitle: String? = null, + @SerialName("original_name") val originalName: String? = null, + val overview: String? = null, + @SerialName("poster_path") val posterPath: String? = null, + @SerialName("backdrop_path") val backdropPath: String? = null, + @SerialName("release_date") val releaseDate: String? = null, + @SerialName("first_air_date") val firstAirDate: String? = null, + val job: String? = null, + @SerialName("vote_average") val voteAverage: Double? = null, + val popularity: Double? = null, +) + @Serializable private data class TmdbListItem( val id: Int,