What 505 real channels showed that 20 demo ones could not
All checks were successful
Build TV app / build (push) Successful in 42s

The favourite count sat at zero while the backend already had two: the rail
read the favourites off the StateFlow inside the composable instead of
collecting it, so it never recomposed — a regression from making favourites
work for both source types. The list collects again.

Real EPG titles broke the fixture shortener in both directions. "BL: VfB
Stuttgart - Saison 25-26" became a fixture against a season, and
"… FC Bayern München - VfB Stuttgart - 1. Halbzeit" grew a third team; a
season, a matchday, a half or a year is now rejected, and a second dash ends
the away side. The kick-off chip also gave way in the row: the line below
already carries the time, and "Bayer Leverkusen - VfB Stuttgart" needed the
width to fit at all.

Merged provider EPGs list the same programme several times, minutes apart —
the day plan showed The Big Bang Theory three times and two entries marked
as running at once. XmltvParser.collapseDuplicates folds a repeat into the
entry whose slot it falls inside.

The login code expired before anyone could confirm it: Authentik hands out a
minute, and the TV dropped back to step one without a word. It fetches a
fresh code instead, up to eight times.

Tested against the household's own Dispatcharr through api.tv.beckm4nn.net.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
be-nj
2026-08-26 16:53:14 +02:00
parent 4cb7a6efdb
commit de6d3a921f
8 changed files with 232 additions and 40 deletions

View File

@@ -0,0 +1,74 @@
package dev.castarr.tv.data
import org.junit.Assert.assertEquals
import org.junit.Test
class DuplicateProgrammesTest {
private fun p(startMin: Int, stopMin: Int, title: String) =
Programme(startMin * 60_000L, stopMin * 60_000L, title)
/** The real case: one channel, one episode, three EPG entries. */
@Test
fun `repeats a few minutes apart collapse into one`() {
val collapsed = XmltvParser.collapseDuplicates(
listOf(
p(1090, 1140, "The Big Bang Theory"),
p(1095, 1145, "The Big Bang Theory"),
p(1120, 1170, "The Big Bang Theory"),
)
)
assertEquals(1, collapsed.size)
assertEquals(1090 * 60_000L, collapsed.first().start)
}
@Test
fun `a genuine repeat later in the evening survives`() {
val collapsed = XmltvParser.collapseDuplicates(
listOf(
p(1090, 1140, "Tagesschau"),
p(1300, 1320, "Tagesschau"),
)
)
assertEquals(2, collapsed.size)
}
@Test
fun `different programmes at the same time both stay`() {
val collapsed = XmltvParser.collapseDuplicates(
listOf(
p(1090, 1140, "Sportschau"),
p(1092, 1142, "Tagesschau"),
)
)
assertEquals(2, collapsed.size)
}
@Test
fun `a repeat inside the first slot collapses even beyond the tolerance`() {
val collapsed = XmltvParser.collapseDuplicates(
listOf(
p(1000, 1200, "Fußball: Konferenz"),
p(1100, 1300, "Fußball: Konferenz"),
)
)
assertEquals(1, collapsed.size)
}
@Test
fun `an unsorted list is handled in time order`() {
val collapsed = XmltvParser.collapseDuplicates(
listOf(
p(1095, 1145, "Two and a Half Men"),
p(1090, 1140, "Two and a Half Men"),
)
)
assertEquals(1, collapsed.size)
assertEquals(1090 * 60_000L, collapsed.first().start)
}
@Test
fun `an empty list stays empty`() {
assertEquals(emptyList<Programme>(), XmltvParser.collapseDuplicates(emptyList()))
}
}