diff --git a/data/src/main/kotlin/voice/data/Chapter.kt b/data/src/main/kotlin/voice/data/Chapter.kt index b128829ad6..6d660d3528 100644 --- a/data/src/main/kotlin/voice/data/Chapter.kt +++ b/data/src/main/kotlin/voice/data/Chapter.kt @@ -27,14 +27,30 @@ data class Chapter( val chapterMarks: List = if (markData.isEmpty()) { listOf(ChapterMark(name, 0L, duration)) } else { - val sorted = markData.sorted() - sorted.mapIndexed { index, (startMs, name) -> - val isFirst = index == 0 - val isLast = index == sorted.size - 1 - val start = if (isFirst) 0L else startMs - val end = if (isLast) duration else sorted[index + 1].startMs - 1 - ChapterMark(name = name, startMs = start, endMs = end) + val sorted = markData.distinctBy { it.startMs } + .filter { it.startMs in 0..() + for ((index, markData) in sorted.withIndex()) { + val name = markData.name + val previous = result.lastOrNull() + val next = sorted.getOrNull(index + 1) + val startMs = if (previous == null) 0L else previous.endMs + 1 + val endMs = if (next != null && next.startMs + 2 < duration && startMs < next.startMs - 1) { + next.startMs - 1 + } else { + duration - 1 + } + result += ChapterMark( + name = name, + startMs = startMs, + endMs = endMs, + ) + if (endMs == duration - 1) { + break + } } + result.ifEmpty { listOf(ChapterMark(name, 0L, duration)) } } override fun compareTo(other: Chapter): Int { diff --git a/data/src/main/kotlin/voice/data/ChapterMark.kt b/data/src/main/kotlin/voice/data/ChapterMark.kt index 2d4e7f8d36..26f630d238 100644 --- a/data/src/main/kotlin/voice/data/ChapterMark.kt +++ b/data/src/main/kotlin/voice/data/ChapterMark.kt @@ -20,6 +20,12 @@ data class ChapterMark( val endMs: Long, ) { + init { + require(startMs < endMs) { + "Start must be less than end in $this" + } + } + operator fun contains(position: Duration): Boolean = position.inWholeMilliseconds in startMs..endMs operator fun contains(positionMs: Long): Boolean = positionMs in startMs..endMs } diff --git a/data/src/test/kotlin/voice/data/ChapterTest.kt b/data/src/test/kotlin/voice/data/ChapterTest.kt new file mode 100644 index 0000000000..7d9564dcb7 --- /dev/null +++ b/data/src/test/kotlin/voice/data/ChapterTest.kt @@ -0,0 +1,31 @@ +package voice.data + +import io.kotest.matchers.collections.shouldContainInOrder +import org.junit.Test +import java.time.Instant + +class ChapterTest { + + @Test + fun `chapter parsing does best effort on broken marks`() { + val positions = Chapter( + duration = 20L, + fileLastModified = Instant.now(), + id = ChapterId(""), + markData = listOf(11, 5, 5, 10).mapIndexed { index, i -> + MarkData( + startMs = i.toLong(), + name = "Mark $index", + ) + }, + name = "Chapter", + ).chapterMarks.map { MarkPosition(it.startMs, it.endMs) } + + positions.shouldContainInOrder( + MarkPosition(0L, 9L), + MarkPosition(10L, 19L), + ) + } + + data class MarkPosition(val start: Long, val end: Long) +} diff --git a/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewState.kt b/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewState.kt index 38995a5665..50956ce15f 100644 --- a/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewState.kt +++ b/playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewState.kt @@ -17,7 +17,14 @@ data class BookPlayViewState( val playing: Boolean, val cover: ImmutableFile?, val skipSilence: Boolean, -) +) { + + init { + require(duration > Duration.ZERO) { + "Duration must be positive in $this" + } + } +} internal sealed interface BookPlayDialogViewState { data class SpeedDialog(