From ae5b82c6fe4ee847495d86e6ddafae28dd9f78ea Mon Sep 17 00:00:00 2001
From: beyond
Date: Mon, 31 Aug 2026 21:09:37 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=A7=E9=9B=86=E8=AF=A6?=
=?UTF-8?q?=E6=83=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../myapplication/data/EmbyRepository.kt | 2 +-
.../myapplication/data/TmdbRepository.kt | 50 ++++++++++---------
.../myapplication/screens/HomeScreen.kt | 11 +++-
.../screens/MediaDetailScreen.kt | 23 ++++++---
.../data/TmdbRecommendationTest.kt | 23 +++++++++
.../screens/HomeScreenDataTest.kt | 19 +++++++
6 files changed, 97 insertions(+), 31 deletions(-)
create mode 100644 app/src/test/kotlin/com/example/myapplication/data/TmdbRecommendationTest.kt
diff --git a/app/src/main/kotlin/com/example/myapplication/data/EmbyRepository.kt b/app/src/main/kotlin/com/example/myapplication/data/EmbyRepository.kt
index 2b7b5b5..5c39111 100644
--- a/app/src/main/kotlin/com/example/myapplication/data/EmbyRepository.kt
+++ b/app/src/main/kotlin/com/example/myapplication/data/EmbyRepository.kt
@@ -254,7 +254,7 @@ object EmbyRepository {
try {
val url = Uri.parse("${server.fullUrl}/emby/Users/${server.userId}/Items/Resume").buildUpon()
.appendQueryParameter("Limit", limit.toString())
- .appendQueryParameter("Fields", "ProductionYear,Genres,UserData")
+ .appendQueryParameter("Fields", "ProductionYear,Genres,UserData,SeriesId,SeriesName")
.appendQueryParameter("MediaTypes", "Video")
.appendQueryParameter("EnableImages", "true")
.appendQueryParameter("EnableUserData", "true")
diff --git a/app/src/main/kotlin/com/example/myapplication/data/TmdbRepository.kt b/app/src/main/kotlin/com/example/myapplication/data/TmdbRepository.kt
index 816fa20..7799fed 100644
--- a/app/src/main/kotlin/com/example/myapplication/data/TmdbRepository.kt
+++ b/app/src/main/kotlin/com/example/myapplication/data/TmdbRepository.kt
@@ -137,16 +137,21 @@ private data class TmdbEpisodeDetail(
private data class TmdbRecommendationItem(
@SerializedName("id") val id: Int = 0,
@SerializedName("name") val name: String = "",
+ @SerializedName("title") val title: String = "",
@SerializedName("original_name") val originalName: String = "",
+ @SerializedName("original_title") val originalTitle: String = "",
@SerializedName("poster_path") val posterPath: String? = null,
@SerializedName("first_air_date") val firstAirDate: String = "",
+ @SerializedName("release_date") val releaseDate: String = "",
@SerializedName("vote_average") val voteAverage: Double = 0.0,
)
+private const val TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w342"
+
object TmdbRepository {
private const val API_BASE = "https://api.tmdb.org/3"
- private const val IMAGE_BASE = "https://image.tmdb.org/t/p/w342"
+ private const val IMAGE_BASE = TMDB_IMAGE_BASE
private val gson = Gson()
private val extrasCache = ConcurrentHashMap()
private val movieExtrasCache = ConcurrentHashMap()
@@ -218,18 +223,8 @@ object TmdbRepository {
.appendQueryParameter("page", "1")
.build()
return get(url.toString(), TmdbRecommendationsResponse::class.java)?.results.orEmpty()
- .filter { it.name.isNotBlank() }
+ .mapNotNull { it.toRecommendation() }
.take(20)
- .map { item ->
- TmdbRecommendation(
- id = item.id,
- name = item.name,
- originalName = item.originalName,
- posterUrl = item.posterPath?.let { IMAGE_BASE + it },
- firstAirYear = item.firstAirDate.take(4),
- rating = item.voteAverage,
- )
- }
}
// ── Movie support ──────────────────────────────────────────────────────
@@ -307,18 +302,8 @@ object TmdbRepository {
.appendQueryParameter("page", "1")
.build()
val result = get(url.toString(), TmdbRecommendationsResponse::class.java)?.results.orEmpty()
- .filter { it.name.isNotBlank() }
+ .mapNotNull { it.toRecommendation() }
.take(20)
- .map { item ->
- TmdbRecommendation(
- id = item.id,
- name = item.name,
- originalName = item.originalName,
- posterUrl = item.posterPath?.let { IMAGE_BASE + it },
- firstAirYear = item.firstAirDate.take(4),
- rating = item.voteAverage,
- )
- }
Log.d("TmdbRepository", "getMovieRecommendations: movieId=$movieId -> ${result.size} recommendations")
return result
}
@@ -437,3 +422,22 @@ object TmdbRepository {
else -> "en-US"
}
}
+
+private fun TmdbRecommendationItem.toRecommendation(): TmdbRecommendation? {
+ val displayName = recommendationTitle(name, title)
+ if (displayName.isBlank()) return null
+ return TmdbRecommendation(
+ id = id,
+ name = displayName,
+ originalName = originalName.ifBlank { originalTitle },
+ posterUrl = posterPath?.let { TMDB_IMAGE_BASE + it },
+ firstAirYear = recommendationYear(firstAirDate, releaseDate),
+ rating = voteAverage,
+ )
+}
+
+internal fun recommendationTitle(name: String, title: String): String =
+ name.ifBlank { title }
+
+internal fun recommendationYear(firstAirDate: String, releaseDate: String): String =
+ firstAirDate.ifBlank { releaseDate }.take(4)
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
index 3a9c9d1..57d142c 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt
@@ -458,6 +458,13 @@ private fun ContinueWatchingPoster(item: EmbyItem, serverUrl: String, onItemClic
}
}
+internal fun continueWatchingDetailId(item: EmbyItem): String =
+ if (item.type.equals("Episode", ignoreCase = true)) {
+ item.seriesId?.takeIf { it.isNotBlank() } ?: item.id
+ } else {
+ item.id
+ }
+
@Composable
private fun LazyLibrarySection(folder: EmbyMediaFolder, activeServer: EmbyServer, onItemClick: (EmbyItem) -> Unit = {}) {
val cacheKey = remember(activeServer.id, activeServer.fullUrl, activeServer.userId) {
@@ -889,7 +896,9 @@ fun HomeScreen(
SectionTitle(title = stringResource(Res.string.continue_watching))
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
items(resumeItems) { item ->
- ContinueWatchingPoster(item, server.fullUrl) { onOpenDetail(it.id) }
+ ContinueWatchingPoster(item, server.fullUrl) {
+ onOpenDetail(continueWatchingDetailId(it))
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt
index 5d9b650..2e78d04 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt
@@ -330,14 +330,18 @@ fun MediaDetailScreen(
val tmdbId = detail.providerIds.entries
.firstOrNull { it.key.equals("Tmdb", ignoreCase = true) }
?.value
- LaunchedEffect(detail.id, detail.name, detail.productionYear, tmdbId, loadingFullDetail) {
- if (!TmdbRepository.isConfigured) return@LaunchedEffect
- if (detail.type == "Series" && loadingFullDetail) {
+ val supportsTmdbExtras = detail.type == "Series" || detail.type == "Movie"
+ LaunchedEffect(detail.id, detail.type, detail.name, detail.productionYear, tmdbId, loadingFullDetail) {
+ if (!TmdbRepository.isConfigured || !supportsTmdbExtras) {
+ loadingTmdb = false
+ return@LaunchedEffect
+ }
+ if (loadingFullDetail) {
loadingTmdb = true
return@LaunchedEffect
}
android.util.Log.d("MediaDetail", "TMDB loading: type=${detail.type}, tmdbId=$tmdbId, name=${detail.name}, year=${detail.productionYear}")
- loadingTmdb = detail.type == "Series"
+ loadingTmdb = true
tmdbExtras = try {
if (detail.type == "Series") {
TmdbRepository.getSeriesExtras(tmdbId, detail.name, detail.productionYear)
@@ -1269,7 +1273,9 @@ private fun CastSection(cast: List, loading: Boolean) {
@Composable
private fun CastMemberCard(member: DetailCastMember) {
Column(
- modifier = Modifier.width(92.dp),
+ modifier = Modifier
+ .width(92.dp)
+ .height(128.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
@@ -1333,7 +1339,12 @@ private fun CastMemberCard(member: DetailCastMember) {
private fun CastSkeletonRow() {
LazyRow(horizontalArrangement = Arrangement.spacedBy(14.dp), userScrollEnabled = false) {
items(4) {
- Column(Modifier.width(92.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(
+ Modifier
+ .width(92.dp)
+ .height(128.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
Box(Modifier.size(88.dp).clip(CircleShape).background(LocalDetailSurface.current))
Spacer(Modifier.height(8.dp))
Box(Modifier.width(62.dp).height(12.dp).clip(RoundedCornerShape(6.dp)).background(LocalDetailSurface.current))
diff --git a/app/src/test/kotlin/com/example/myapplication/data/TmdbRecommendationTest.kt b/app/src/test/kotlin/com/example/myapplication/data/TmdbRecommendationTest.kt
new file mode 100644
index 0000000..152648b
--- /dev/null
+++ b/app/src/test/kotlin/com/example/myapplication/data/TmdbRecommendationTest.kt
@@ -0,0 +1,23 @@
+package com.example.myapplication.data
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TmdbRecommendationTest {
+
+ @Test
+ fun `movie recommendation uses movie title when tv name is absent`() {
+ assertEquals("Movie title", recommendationTitle("", "Movie title"))
+ }
+
+ @Test
+ fun `movie recommendation uses release year when tv air date is absent`() {
+ assertEquals("2024", recommendationYear("", "2024-06-01"))
+ }
+
+ @Test
+ fun `tv recommendation keeps tv name and air year`() {
+ assertEquals("Series title", recommendationTitle("Series title", "Movie title"))
+ assertEquals("2023", recommendationYear("2023-01-01", "2024-06-01"))
+ }
+}
diff --git a/app/src/test/kotlin/com/example/myapplication/screens/HomeScreenDataTest.kt b/app/src/test/kotlin/com/example/myapplication/screens/HomeScreenDataTest.kt
index a56b393..f6ea67c 100644
--- a/app/src/test/kotlin/com/example/myapplication/screens/HomeScreenDataTest.kt
+++ b/app/src/test/kotlin/com/example/myapplication/screens/HomeScreenDataTest.kt
@@ -25,6 +25,25 @@ class HomeScreenDataTest {
assertEquals("The Movie", heroDisplayTitle(movie))
}
+ @Test
+ fun continueWatchingEpisodeOpensSeriesDetail() {
+ val episode = EmbyItem(
+ id = "episode-1",
+ name = "Episode 7",
+ type = "Episode",
+ seriesId = "series-1",
+ )
+
+ assertEquals("series-1", continueWatchingDetailId(episode))
+ }
+
+ @Test
+ fun continueWatchingMovieKeepsMovieDetail() {
+ val movie = EmbyItem(id = "movie-1", name = "The Movie", type = "Movie")
+
+ assertEquals("movie-1", continueWatchingDetailId(movie))
+ }
+
@Test
fun elapsedPlaybackFormatsMilliseconds() {
assertEquals("12:34", formatPlaybackElapsed(754_000L))