Big changes

This commit is contained in:
ponzischeme89
2026-07-29 15:26:27 +12:00
parent 8d6cf2f5a1
commit 70914400b4
62 changed files with 7501 additions and 744 deletions
@@ -1,9 +1,13 @@
package com.ponzischeme89.memby.data
import com.ponzischeme89.memby.data.model.GatewayHome
import com.ponzischeme89.memby.data.model.GatewayNextEpisode
import com.ponzischeme89.memby.data.model.GatewayPlayback
import com.ponzischeme89.memby.data.model.GatewaySearchHistory
import com.ponzischeme89.memby.data.model.GatewayServiceStatus
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -23,6 +27,17 @@ class GatewayPayloadTest {
explicitNulls = false
}
@Test
fun `decodes explicit client server protocol mismatch`() {
val status = json.decodeFromString<GatewayServiceStatus>(
"""{"maintenance":false,"compatible":false,"compatibilityMessage":"App protocol 2, server protocol 1.","clientVersion":"0.9.1","clientProtocol":"2","serverProtocol":1}""",
)
assertEquals(false, status.compatible)
assertEquals("App protocol 2, server protocol 1.", status.compatibilityMessage)
assertEquals(1, status.serverProtocol)
}
@Test
fun `decodes a home payload with emby-shaped items`() {
val payload = """
@@ -79,6 +94,30 @@ class GatewayPayloadTest {
assertEquals("Solaris", home.rows.last().items.single().name)
}
@Test
fun `decodes explainable For You metadata`() {
val payload = """
{
"rows":[{
"id":"for-you:picks",
"title":"Top picks that fit in 60 minutes",
"kind":"for-you",
"items":[{
"Id":"9","Name":"Pilot","Type":"Episode",
"MembyRecommendationReason":"Matches your Drama viewing · fits your 60-minute window",
"MembyCompatibility":"Direct plays well on this TV"
}]
}]
}
""".trimIndent()
val item = json.decodeFromString<com.ponzischeme89.memby.data.model.GatewayRows>(payload)
.rows.single().items.single()
assertTrue(item.membyRecommendationReason!!.contains("60-minute"))
assertEquals("Direct plays well on this TV", item.membyCompatibility)
}
@Test
fun `decodes the informational Sonarr schedule row`() {
val payload = """
@@ -135,6 +174,15 @@ class GatewayPayloadTest {
assertTrue(home.partial)
}
@Test
fun `decodes persisted recent searches`() {
val history = json.decodeFromString<GatewaySearchHistory>(
"""{"queries":["severance","slow horses","arrival"]}""",
)
assertEquals(listOf("severance", "slow horses", "arrival"), history.queries)
}
@Test
fun `decodes a playback response`() {
val playback = json.decodeFromString<GatewayPlayback>(
@@ -145,6 +193,48 @@ class GatewayPayloadTest {
assertTrue(playback.url.startsWith("https://emby.example/Videos/9/stream"))
}
@Test
fun `decodes the next-episode response the player counts down to`() {
val next = json.decodeFromString<GatewayNextEpisode>(
"""
{
"item": {
"Id":"11","Name":"The Bicameral Mind","Type":"Episode",
"SeriesName":"Westworld","SeriesId":"7",
"IndexNumber":5,"ParentIndexNumber":2,
"ImageTags":{"Primary":"abc"}
},
"title": "Westworld The Bicameral Mind",
"url": "https://emby.example/Videos/11/stream?static=true",
"resumePositionMs": 0
}
""".trimIndent(),
)
assertEquals("11", next.item.id)
assertEquals("Westworld", next.item.seriesName)
// The banner's subtitle is built from these, so they have to survive the wire.
assertEquals("S2 · E5", next.item.episodeCode)
assertEquals(0L, next.resumePositionMs)
assertTrue(next.url.startsWith("https://emby.example/Videos/11/stream"))
}
@Test
fun `an episode without a season still gets a usable code`() {
val next = json.decodeFromString<GatewayNextEpisode>(
"""{"item":{"Id":"12","Name":"Special","Type":"Episode","IndexNumber":3},"url":"https://e/x"}""",
)
assertEquals("E3", next.item.episodeCode)
}
@Test
fun `an item with no episode numbering has no code`() {
val next = json.decodeFromString<GatewayNextEpisode>(
"""{"item":{"Id":"13","Name":"Arrival","Type":"Movie"},"url":"https://e/x"}""",
)
assertNull(next.item.episodeCode)
}
@Test
fun `device allowance response becomes a specific sign-in error`() {
val error = parseDeviceLimit(
@@ -1,5 +1,6 @@
package com.ponzischeme89.memby.data
import com.ponzischeme89.memby.data.model.DeviceProfile
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -13,4 +14,16 @@ class PlaybackReportMathTest {
fun negativePositionsAreClamped() {
assertEquals(0L, millisecondsToTicks(-1L))
}
@Test
fun deviceProfileExternalizesTextAndEncodesBitmapSubtitles() {
val profiles = DeviceProfile.embyAndroidTv().subtitleProfiles.associate {
it.format to it.method
}
assertEquals("External", profiles["srt"])
assertEquals("External", profiles["ass"])
assertEquals("External", profiles["mov_text"])
assertEquals("Encode", profiles["pgssub"])
assertEquals("Encode", profiles["dvdsub"])
}
}
@@ -36,4 +36,21 @@ class ProfileSettingsTest {
assertNull(settings.activeProfileId)
}
@Test
fun `for you choices belong to each profile`() {
val matt = profile.copy(forYouMinutes = 60, hasOpenedForYou = true)
val family = profile.copy(
id = "server::family",
userId = "family",
username = "FamilyTV",
forYouMinutes = 30,
hasOpenedForYou = false,
)
assertEquals(60, matt.forYouMinutes)
assertEquals(true, matt.hasOpenedForYou)
assertEquals(30, family.forYouMinutes)
assertEquals(false, family.hasOpenedForYou)
}
}
@@ -16,6 +16,8 @@ import org.junit.Test
*/
class ServerHomeRowsTest {
private val json = Json { ignoreUnknownKeys = true }
private fun row(id: String, kind: String, vararg itemIds: String) = HomeRow(
id = id,
title = id.replaceFirstChar(Char::uppercase),
@@ -69,6 +71,14 @@ class ServerHomeRowsTest {
assertEquals("Favorites", personalizedFavoritesTitle(null))
}
@Test
fun `successful login welcome uses authenticated username`() {
assertEquals(
"You're now logged in as Matt. Welcome to Memby!",
loginWelcomeMessage(" Matt "),
)
}
@Test
fun `disabling a section hides its rows but never the recommendations`() {
val settings = Settings(homeSections = "continue")
@@ -93,6 +103,91 @@ class ServerHomeRowsTest {
assertEquals(MediaRowKind.MOVIES, rows.getValue("similar:sev").kind)
}
@Test
fun `For You destination keeps reasons visible and exposes loading placeholder`() {
val loading = forYouBrowseRows(ForYouUiState(loading = true))
assertEquals(1, loading.size)
assertTrue(loading.single().loading)
val rows = forYouBrowseRows(
ForYouUiState(
rows = listOf(
row("for-you:picks", "for-you", "pick"),
row("for-you:because:six-feet-under", "for-you", "because"),
row("for-you:genre:drama", "for-you", "genre"),
),
availableMinutes = 60,
),
)
assertEquals(
listOf(
"for-you:picks",
"for-you:because:six-feet-under",
"for-you:genre:drama",
),
rows.map { it.id },
)
assertTrue(rows.all { it.kind == MediaRowKind.MOVIES })
assertTrue(rows.all { it.showSecondaryMetadata })
}
@Test
fun `shows destination includes empty favorites and ranked server shelves`() {
val rankedRows = listOf(
row("curated:comedy-shows", "shows", "c1"),
row("curated:horror-shows", "shows", "h1"),
row("curated:drama-shows", "shows", "d1"),
)
val state = HomeUiState(
rows = serverRows + rankedRows,
favorites = emptyList(),
loading = emptySet(),
)
val rows = homeRowsFor(BrowseDestination.SHOWS, state, Settings())
assertEquals(
listOf(
"next-up",
"continue-shows",
"favourite-shows",
"curated:comedy-shows",
"curated:horror-shows",
"curated:drama-shows",
),
rows.map { it.id },
)
assertTrue(rows.first { it.id == "favourite-shows" }.items.isEmpty())
}
@Test
fun `movies destination uses personalised genre and studio shelves`() {
val rankedRows = listOf(
row("curated:movies:studio:pixar", "movies", "p1"),
row("curated:movies:genre:animation", "movies", "a1"),
row("curated:drama-shows", "shows", "s1"),
row("recommended", "recommended", "mixed"),
)
val state = HomeUiState(
rows = serverRows + rankedRows,
favorites = listOf(BaseItem(id = "f1", type = "Movie")),
latestMovies = listOf(BaseItem(id = "latest", type = "Movie")),
loading = emptySet(),
)
val rows = homeRowsFor(BrowseDestination.MOVIES, state, Settings())
assertEquals(
listOf(
"curated:movies:studio:pixar",
"curated:movies:genre:animation",
"favourite-movies",
),
rows.map { it.id },
)
assertTrue(rows.none { it.id == "latest-movies" || it.id == "recommended" })
}
@Test
fun `Sonarr schedule rows use show cards and cannot be hidden by old preferences`() {
val schedule = row("sonarr-airing-today", "schedule", "sonarr:7:42")
@@ -160,7 +255,7 @@ class ServerHomeRowsTest {
fun `a cache written before rows existed still decodes`() {
val legacy = """{"continueWatching":[{"Id":"a"}],"favorites":[],"nextUp":[],"latestMovies":[]}"""
val restored = HomeUiState.from(Json { ignoreUnknownKeys = true }.decodeFromString<HomeCache>(legacy))
val restored = HomeUiState.from(json.decodeFromString<HomeCache>(legacy))
assertTrue(restored.rows.isEmpty())
assertEquals("a", restored.continueWatching.single().id)