refactor: centralize domain models into core.model module
This commit is contained in:
@@ -42,6 +42,7 @@ kotlin {
|
||||
// Core modules
|
||||
implementation(projects.core.coroutines)
|
||||
implementation(projects.core.logging)
|
||||
implementation(projects.core.model)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutinesCore)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.data
|
||||
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
@@ -14,23 +12,23 @@ internal class LrcParser {
|
||||
private val timeTag = Regex("""\[(\d{1,2}):(\d{2})[.:](\d{2,3})]""")
|
||||
private val leadingTimeTags = Regex("""^(?:\[\d{1,2}:\d{2}[.:]\d{2,3}])+""")
|
||||
|
||||
fun parse(content: String): Subtitle {
|
||||
val lines = content
|
||||
fun parse(content: String): List<TimeCodedText> =
|
||||
content
|
||||
.lineSequence()
|
||||
.flatMap { line -> parseLine(line) }
|
||||
.sortedBy { it.startTime }
|
||||
.toList()
|
||||
return Subtitle(lines)
|
||||
}
|
||||
|
||||
private fun parseLine(line: String): List<SubtitleLine> {
|
||||
private fun parseLine(line: String): List<TimeCodedText> {
|
||||
val tags = leadingTimeTags.find(line) ?: return emptyList()
|
||||
val text = line.substring(tags.range.last + 1).trim()
|
||||
return timeTag
|
||||
.findAll(tags.value)
|
||||
.mapNotNull { match ->
|
||||
SubtitleLine(startTime = match.toMs().milliseconds, text = text)
|
||||
.takeIf { it.text.isNotBlank() }
|
||||
TimeCodedText(
|
||||
startTime = match.toMs().milliseconds,
|
||||
text = text,
|
||||
).takeIf { text.isNotBlank() }
|
||||
}.toList()
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -2,9 +2,11 @@ package fr.ajaury.gwenedeg.subtitle.data
|
||||
|
||||
import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider
|
||||
import fr.ajaury.gwenedeg.core.logging.domain.Logger
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
||||
import fr.ajaury.gwenedeg.subtitle.model.CantGetSubtitleException
|
||||
import fr.ajaury.gwenedeg.subtitle.model.Subtitle
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class LrcSubtitleRepository(
|
||||
@@ -13,28 +15,33 @@ internal class LrcSubtitleRepository(
|
||||
private val logger: Logger,
|
||||
) : SubtitleRepository {
|
||||
override suspend fun getSubtitle(
|
||||
content: String,
|
||||
transcriptionContent: String,
|
||||
translationContent: String?,
|
||||
): Subtitle =
|
||||
withContext(dispatcherProvider.default) {
|
||||
try {
|
||||
val subtitle = parser.parse(content = content)
|
||||
val transcription = parser.parse(content = transcriptionContent)
|
||||
val translation = translationContent?.let { parser.parse(content = it) }
|
||||
subtitle.mergeTranslation(translation)
|
||||
transcription.mergeTranslation(translation)
|
||||
} catch (exception: Exception) {
|
||||
val message = "Failed to load subtitle: $content"
|
||||
val message = "Failed to load subtitle: $transcriptionContent"
|
||||
logger.error(message = message, throwable = exception)
|
||||
throw CantGetSubtitleException(message)
|
||||
}
|
||||
}
|
||||
|
||||
/** Attaches each [translation] line's text to the matching original line, paired by timestamp. */
|
||||
private fun Subtitle.mergeTranslation(translation: Subtitle?): Subtitle {
|
||||
if (translation == null) return this
|
||||
val translationByStart = translation.lines.associate { it.startTime to it.text }
|
||||
private fun List<TimeCodedText>.mergeTranslation(translation: List<TimeCodedText>?): Subtitle {
|
||||
val translationByStart = translation?.associate { it.startTime to it.text } ?: emptyMap()
|
||||
return Subtitle(
|
||||
lines = lines.map { line ->
|
||||
line.copy(translation = translationByStart[line.startTime])
|
||||
lines = this.map {
|
||||
SubtitleLine(
|
||||
startTime = it.startTime,
|
||||
phrase = Phrase(
|
||||
transcription = it.text,
|
||||
translation = translationByStart[it.startTime],
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.data
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A single timed text cue: [text] becomes active at [startTime].
|
||||
*/
|
||||
data class TimeCodedText(
|
||||
val startTime: Duration,
|
||||
val text: String,
|
||||
)
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.domain
|
||||
|
||||
import fr.ajaury.gwenedeg.subtitle.model.Subtitle
|
||||
import kotlin.time.Duration
|
||||
|
||||
class GetCurrentSubtitleIndexUseCase {
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.domain
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A single timed subtitle cue: [text] becomes active at [startTime]. [translation] is the optional
|
||||
* translation of [text] in the user's language (e.g. French), shown alongside the original.
|
||||
*/
|
||||
data class SubtitleLine(
|
||||
val startTime: Duration,
|
||||
val text: String,
|
||||
val translation: String? = null,
|
||||
)
|
||||
+4
-2
@@ -1,12 +1,14 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.domain
|
||||
|
||||
import fr.ajaury.gwenedeg.subtitle.model.Subtitle
|
||||
|
||||
interface SubtitleRepository {
|
||||
/**
|
||||
* Parses the subtitle [content]. When [translationContent] is provided, each line is matched to
|
||||
* Parses the subtitle [transcriptionContent]. When [translationContent] is provided, each line is matched to
|
||||
* its translation by timestamp.
|
||||
*/
|
||||
suspend fun getSubtitle(
|
||||
content: String,
|
||||
transcriptionContent: String,
|
||||
translationContent: String? = null,
|
||||
): Subtitle
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.domain
|
||||
package fr.ajaury.gwenedeg.subtitle.model
|
||||
|
||||
data class Subtitle(
|
||||
val lines: List<SubtitleLine>,
|
||||
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.subtitle.model
|
||||
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A single timed subtitle cue: [phrase] becomes active at [startTime].
|
||||
*/
|
||||
data class SubtitleLine(
|
||||
val startTime: Duration,
|
||||
val phrase: Phrase,
|
||||
)
|
||||
+6
-5
@@ -1,8 +1,9 @@
|
||||
package fr.ajaury.gwenedeg.subtitle
|
||||
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine
|
||||
import fr.ajaury.gwenedeg.subtitle.model.Subtitle
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -14,9 +15,9 @@ class GetCurrentSubtitleIndexUseCaseTest {
|
||||
Subtitle(
|
||||
lines =
|
||||
listOf(
|
||||
SubtitleLine(startTime = 0.milliseconds, text = "Demat deoc'h !"),
|
||||
SubtitleLine(startTime = 4310.milliseconds, text = "Kenavo !"),
|
||||
SubtitleLine(startTime = 7350.milliseconds, text = "Kenavo emberr !"),
|
||||
SubtitleLine(startTime = 0.milliseconds, phrase = Phrase(transcription = "Demat deoc'h !")),
|
||||
SubtitleLine(startTime = 4310.milliseconds, phrase = Phrase(transcription = "Kenavo !")),
|
||||
SubtitleLine(startTime = 7350.milliseconds, phrase = Phrase(transcription = "Kenavo emberr !")),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -23,31 +23,31 @@ class LrcParserTest {
|
||||
|
||||
@Test
|
||||
fun ignores_metadata_tags_and_keeps_only_timed_cues() {
|
||||
val subtitle = parser.parse(sample)
|
||||
val lines = parser.parse(sample)
|
||||
|
||||
assertEquals(3, subtitle.lines.size)
|
||||
assertEquals(3, lines.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun converts_timestamp_to_milliseconds() {
|
||||
val subtitle = parser.parse(sample)
|
||||
val lines = parser.parse(sample)
|
||||
|
||||
assertEquals(0.milliseconds, subtitle.lines[0].startTime)
|
||||
assertEquals(4310.milliseconds, subtitle.lines[1].startTime)
|
||||
assertEquals(7350.milliseconds, subtitle.lines[2].startTime)
|
||||
assertEquals(0.milliseconds, lines[0].startTime)
|
||||
assertEquals(4310.milliseconds, lines[1].startTime)
|
||||
assertEquals(7350.milliseconds, lines[2].startTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parses_three_digit_fraction_as_milliseconds() {
|
||||
val subtitle = parser.parse("[00:01.250]Hello")
|
||||
val lines = parser.parse("[00:01.250]Hello")
|
||||
|
||||
assertEquals(1250.milliseconds, subtitle.lines.single().startTime)
|
||||
assertEquals(1250.milliseconds, lines.single().startTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sorts_cues_ascending_by_start() {
|
||||
val subtitle = parser.parse("[00:05.00]Later\n[00:01.00]Earlier")
|
||||
val lines = parser.parse("[00:05.00]Later\n[00:01.00]Earlier")
|
||||
|
||||
assertEquals(listOf("Earlier", "Later"), subtitle.lines.map { it.text })
|
||||
assertEquals(listOf("Earlier", "Later"), lines.map { it.text })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user