Fixed projection

This commit is contained in:
Brage Skjønborg 2026-01-31 21:31:07 +01:00
parent ed6bddb95f
commit 438044faaf
2 changed files with 178 additions and 63 deletions

View File

@ -6,11 +6,18 @@ import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.*
import no.iktdev.mediaprocessing.shared.common.resolveConflict import no.iktdev.mediaprocessing.shared.common.resolveConflict
import java.io.File import java.io.File
open class MigrateContentProject(val events: List<Event>, val storageArea: File) { open class MigrateContentProject(
val events: List<Event>,
val storageArea: File
) {
val useStore: File? = getDesiredStoreFolder() val useStore: File? = getDesiredStoreFolder()
open fun getFoldersInStore(): List<String> { open fun getFoldersInStore(): List<String> {
return storageArea.listFiles { file -> file.isDirectory }?.map { it.name }?.toList() ?: emptyList() return storageArea
.listFiles { file -> file.isDirectory }
?.map { it.name }
?: emptyList()
} }
internal fun getFileName(): String? { internal fun getFileName(): String? {
@ -19,75 +26,77 @@ open class MigrateContentProject(val events: List<Event>, val storageArea: File)
} }
internal fun getDesiredStoreFolder(): File? { internal fun getDesiredStoreFolder(): File? {
val assuredCollection = getDesiredCollection() ?: return null val desiredCollection = getDesiredCollection() ?: return null
val assuredStore = storageArea.using(assuredCollection) val assuredStore = storageArea.using(desiredCollection)
val existingCollectionNames = getFoldersInStore().ifEmpty {
val existingCollectionNames = getFoldersInStore()
if (existingCollectionNames.isEmpty()) {
return assuredStore return assuredStore
} }
val titles = getMetadataTitles() val titles = getMetadataTitles()
val existingCollection = titles.filter { it in existingCollectionNames }.firstOrNull()?.let {
File(it) val matchedExisting = titles
} ?: assuredStore .firstOrNull { it in existingCollectionNames }
return existingCollection ?.let { storageArea.using(it) }
return matchedExisting ?: assuredStore
} }
internal fun getMetadataTitles(): List<String> { internal fun getMetadataTitles(): List<String> {
val metadataEvent = events.filterIsInstance<MetadataSearchResultEvent>().lastOrNull()?.recommended?.metadata val metadataEvent = events
.filterIsInstance<MetadataSearchResultEvent>()
.lastOrNull()
?.recommended
?.metadata
?: return emptyList() ?: return emptyList()
return (metadataEvent.alternateTitles + listOf(metadataEvent.title))
return metadataEvent.alternateTitles + metadataEvent.title
} }
internal fun getDesiredCollection(): String? { internal fun getDesiredCollection(): String? {
val metadataEvent = events.filterIsInstance<MediaParsedInfoEvent>().lastOrNull()?.data return events
return metadataEvent?.parsedCollection .filterIsInstance<MediaParsedInfoEvent>()
.lastOrNull()
?.data
?.parsedCollection
} }
fun getVideoStoreFile(): CachedToStore? { fun getVideoStoreFile(): CachedToStore? {
val encoded = events.filterIsInstance<ProcesserEncodeResultEvent>().lastOrNull() val encoded = events.filterIsInstance<ProcesserEncodeResultEvent>().lastOrNull()
val cached = encoded?.data?.cachedOutputFile?.let { File(it) } ?: return null val cached = encoded?.data?.cachedOutputFile?.let(::File) ?: return null
val useFilename = getFileName()?.let { "$it.${cached.extension}" } ?: return null
val store = useStore?.using(useFilename) ?: return null val filename = getFileName()?.let { "$it.${cached.extension}" } ?: return null
return CachedToStore( val store = useStore?.using(filename) ?: return null
cachedFile = cached,
storeFile = store return CachedToStore(cachedFile = cached, storeFile = store)
)
} }
fun getSubtitleStoreFiles(): List<CachedToStoreLanguage>? { fun getSubtitleStoreFiles(): List<CachedToStoreLanguage>? {
val extractedFiles: Map<String, List<File>> = events val extractedFiles = events
.filterIsInstance<ProcesserExtractResultEvent>() .filterIsInstance<ProcesserExtractResultEvent>()
.mapNotNull { it.data } .mapNotNull { it.data }
.map { data -> data.language to File(data.cachedOutputFile) } .map { it.language to File(it.cachedOutputFile) }
.groupBy({ it.first }, { it.second }) .groupBy({ it.first }, { it.second })
val convertedFilesGrouped: Map<String, List<File>> = events val convertedFiles = events
.filterIsInstance<ConvertTaskResultEvent>() .filterIsInstance<ConvertTaskResultEvent>()
.mapNotNull { it.data } .mapNotNull { it.data }
.map { x -> x.outputFiles.map { x.language to File(it) } } .flatMap { d -> d.outputFiles.map { d.language to File(it) } }
.flatten()
.groupBy({ it.first }, { it.second }) .groupBy({ it.first }, { it.second })
val byLanguage = mutableMapOf<String, MutableList<File>>() val byLanguage = mutableMapOf<String, MutableList<File>>()
extractedFiles.forEach { (lang, files) -> extractedFiles.forEach { (lang, files) -> byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) }
byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) convertedFiles.forEach { (lang, files) -> byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) }
}
convertedFilesGrouped.forEach { (lang, files) ->
byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files)
}
val useFilename = getFileName() ?: return null val baseName = getFileName() ?: return null
return byLanguage.flatMap { (language, files) -> return byLanguage.flatMap { (language, files) ->
files.mapNotNull { cached -> files.mapNotNull { cached ->
val useFilename = "$useFilename.${cached.extension}" val filename = "$baseName.${cached.extension}"
val store = useStore?.using(language, useFilename) ?: return@mapNotNull null val store = useStore?.using(language, filename) ?: return@mapNotNull null
CachedToStoreLanguage( CachedToStoreLanguage(
CachedToStore( cts = CachedToStore(cachedFile = cached, storeFile = store),
cachedFile = cached,
storeFile = store
),
language = language language = language
) )
} }
@ -95,10 +104,11 @@ open class MigrateContentProject(val events: List<Event>, val storageArea: File)
} }
fun getCoverStoreFiles(): List<CachedToStore>? { fun getCoverStoreFiles(): List<CachedToStore>? {
val downloaded = events.filterIsInstance<CoverDownloadResultEvent>() val downloaded = events
.mapNotNull { event -> .filterIsInstance<CoverDownloadResultEvent>()
val file = event.data?.outputFile?.let(::File) ?: return@mapNotNull null .mapNotNull { e ->
event to file val file = e.data?.outputFile?.let(::File) ?: return@mapNotNull null
e to file
} }
val baseName = getDesiredCollection() ?: return null val baseName = getDesiredCollection() ?: return null
@ -110,10 +120,7 @@ open class MigrateContentProject(val events: List<Event>, val storageArea: File)
val ext = cached.extension val ext = cached.extension
val source = event.data?.source ?: "unknown" val source = event.data?.source ?: "unknown"
// Bestem om vi skal bruke source i navnet val filename = if (multiple || store.using("$baseName.$ext").exists()) {
val useSource = multiple || store.using("$baseName.$ext").exists()
val filename = if (useSource) {
"$baseName-$source.$ext" "$baseName-$source.$ext"
} else { } else {
"$baseName.$ext" "$baseName.$ext"
@ -121,21 +128,10 @@ open class MigrateContentProject(val events: List<Event>, val storageArea: File)
val storeFile = store.using(filename).resolveConflict() val storeFile = store.using(filename).resolveConflict()
CachedToStore( CachedToStore(cachedFile = cached, storeFile = storeFile)
cachedFile = cached,
storeFile = storeFile
)
} }
} }
data class CachedToStore(val cachedFile: File, val storeFile: File)
data class CachedToStore( data class CachedToStoreLanguage(val cts: CachedToStore, val language: String)
val cachedFile: File,
val storeFile: File
)
data class CachedToStoreLanguage(
val cts: CachedToStore,
val language: String
)
} }

View File

@ -0,0 +1,119 @@
package no.iktdev.mediaprocessing.shared.common.projection
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.models.store.TaskStatus
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaParsedInfoEvent
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaParsedInfoEvent.ParsedData
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MetadataSearchResultEvent
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodeResultEvent
import no.iktdev.mediaprocessing.shared.common.model.MediaType
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.io.File
class MigrateContentProjectPathTest {
private fun tempDir(): File {
val dir = createTempDir(prefix = "store-test-")
dir.deleteOnExit()
return dir
}
@Test
@DisplayName(
"""
Når projeksjonen beregner video-path
:
Skal pathen alltid ligge under storageArea
"""
)
fun videoPathIsUnderStorageArea() {
val storage = tempDir()
val events = listOf<Event>(
MediaParsedInfoEvent(
ParsedData(
parsedCollection = "Breaking Bad",
parsedFileName = "bb.s01e01",
parsedSearchTitles = emptyList(),
mediaType = MediaType.Serie
)
),
ProcesserEncodeResultEvent(
status = TaskStatus.Completed,
data = ProcesserEncodeResultEvent.EncodeResult(
cachedOutputFile = "/tmp/encoded.mp4"
)
)
)
val project = MigrateContentProject(events, storage)
val video = project.getVideoStoreFile()
assertNotNull(video)
val storeFile = video!!.storeFile
assertTrue(
storeFile.absolutePath.startsWith(storage.absolutePath),
"Store path must be inside storageArea"
)
assertEquals(
File(storage, "Breaking Bad/bb.s01e01.mp4").absolutePath,
storeFile.absolutePath
)
}
@Test
@DisplayName(
"""
Når metadata-titler matcher eksisterende mapper
:
Skal projeksjonen bruke eksisterende mappe
"""
)
fun usesExistingCollectionFolder() {
val storage = tempDir()
val existing = File(storage, "Breaking Bad")
existing.mkdirs()
val events = listOf<Event>(
MediaParsedInfoEvent(
ParsedData(
parsedCollection = "bb",
parsedFileName = "bb.s01e01",
parsedSearchTitles = emptyList(),
mediaType = MediaType.Serie
)
),
MetadataSearchResultEvent(
results = emptyList(),
recommended = MetadataSearchResultEvent.SearchResult(
simpleScore = 10,
prefixScore = 10,
advancedScore = 10,
sourceWeight = 1f,
metadata = MetadataSearchResultEvent.SearchResult.MetadataResult(
source = "tmdb",
title = "Breaking Bad",
alternateTitles = listOf("BB"),
cover = "x.jpg",
type = MediaType.Serie,
summary = emptyList(),
genres = emptyList()
)
),
status = TaskStatus.Completed
)
)
val project = MigrateContentProject(events, storage)
assertEquals(
existing.absolutePath,
project.useStore!!.absolutePath
)
}
}