From 438044faafc8fd405f9b15b3f5f0217d8156e6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brage=20Skj=C3=B8nborg?= Date: Sat, 31 Jan 2026 21:31:07 +0100 Subject: [PATCH] Fixed projection --- .../projection/MigrateContentProject.kt | 122 +++++++++--------- .../projection/MigrateContentProjectTest.kt | 119 +++++++++++++++++ 2 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProjectTest.kt diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProject.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProject.kt index 47315729..e9d4bd53 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProject.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProject.kt @@ -6,11 +6,18 @@ import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.* import no.iktdev.mediaprocessing.shared.common.resolveConflict import java.io.File -open class MigrateContentProject(val events: List, val storageArea: File) { +open class MigrateContentProject( + val events: List, + val storageArea: File +) { + val useStore: File? = getDesiredStoreFolder() open fun getFoldersInStore(): List { - 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? { @@ -19,75 +26,77 @@ open class MigrateContentProject(val events: List, val storageArea: File) } internal fun getDesiredStoreFolder(): File? { - val assuredCollection = getDesiredCollection() ?: return null - val assuredStore = storageArea.using(assuredCollection) - val existingCollectionNames = getFoldersInStore().ifEmpty { + val desiredCollection = getDesiredCollection() ?: return null + val assuredStore = storageArea.using(desiredCollection) + + val existingCollectionNames = getFoldersInStore() + if (existingCollectionNames.isEmpty()) { return assuredStore } + val titles = getMetadataTitles() - val existingCollection = titles.filter { it in existingCollectionNames }.firstOrNull()?.let { - File(it) - } ?: assuredStore - return existingCollection + + val matchedExisting = titles + .firstOrNull { it in existingCollectionNames } + ?.let { storageArea.using(it) } + + return matchedExisting ?: assuredStore } internal fun getMetadataTitles(): List { - val metadataEvent = events.filterIsInstance().lastOrNull()?.recommended?.metadata + val metadataEvent = events + .filterIsInstance() + .lastOrNull() + ?.recommended + ?.metadata ?: return emptyList() - return (metadataEvent.alternateTitles + listOf(metadataEvent.title)) + + return metadataEvent.alternateTitles + metadataEvent.title } internal fun getDesiredCollection(): String? { - val metadataEvent = events.filterIsInstance().lastOrNull()?.data - return metadataEvent?.parsedCollection + return events + .filterIsInstance() + .lastOrNull() + ?.data + ?.parsedCollection } - fun getVideoStoreFile(): CachedToStore? { val encoded = events.filterIsInstance().lastOrNull() - val cached = encoded?.data?.cachedOutputFile?.let { File(it) } ?: return null - val useFilename = getFileName()?.let { "$it.${cached.extension}" } ?: return null - val store = useStore?.using(useFilename) ?: return null - return CachedToStore( - cachedFile = cached, - storeFile = store - ) + val cached = encoded?.data?.cachedOutputFile?.let(::File) ?: return null + + val filename = getFileName()?.let { "$it.${cached.extension}" } ?: return null + val store = useStore?.using(filename) ?: return null + + return CachedToStore(cachedFile = cached, storeFile = store) } fun getSubtitleStoreFiles(): List? { - val extractedFiles: Map> = events + val extractedFiles = events .filterIsInstance() .mapNotNull { it.data } - .map { data -> data.language to File(data.cachedOutputFile) } + .map { it.language to File(it.cachedOutputFile) } .groupBy({ it.first }, { it.second }) - val convertedFilesGrouped: Map> = events + val convertedFiles = events .filterIsInstance() .mapNotNull { it.data } - .map { x -> x.outputFiles.map { x.language to File(it) } } - .flatten() + .flatMap { d -> d.outputFiles.map { d.language to File(it) } } .groupBy({ it.first }, { it.second }) - val byLanguage = mutableMapOf>() - extractedFiles.forEach { (lang, files) -> - byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) - } - convertedFilesGrouped.forEach { (lang, files) -> - byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) - } + extractedFiles.forEach { (lang, files) -> byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) } + convertedFiles.forEach { (lang, files) -> byLanguage.getOrPut(lang) { mutableListOf() }.addAll(files) } - val useFilename = getFileName() ?: return null + val baseName = getFileName() ?: return null return byLanguage.flatMap { (language, files) -> files.mapNotNull { cached -> - val useFilename = "$useFilename.${cached.extension}" - val store = useStore?.using(language, useFilename) ?: return@mapNotNull null + val filename = "$baseName.${cached.extension}" + val store = useStore?.using(language, filename) ?: return@mapNotNull null CachedToStoreLanguage( - CachedToStore( - cachedFile = cached, - storeFile = store - ), + cts = CachedToStore(cachedFile = cached, storeFile = store), language = language ) } @@ -95,10 +104,11 @@ open class MigrateContentProject(val events: List, val storageArea: File) } fun getCoverStoreFiles(): List? { - val downloaded = events.filterIsInstance() - .mapNotNull { event -> - val file = event.data?.outputFile?.let(::File) ?: return@mapNotNull null - event to file + val downloaded = events + .filterIsInstance() + .mapNotNull { e -> + val file = e.data?.outputFile?.let(::File) ?: return@mapNotNull null + e to file } val baseName = getDesiredCollection() ?: return null @@ -110,10 +120,7 @@ open class MigrateContentProject(val events: List, val storageArea: File) val ext = cached.extension val source = event.data?.source ?: "unknown" - // Bestem om vi skal bruke source i navnet - val useSource = multiple || store.using("$baseName.$ext").exists() - - val filename = if (useSource) { + val filename = if (multiple || store.using("$baseName.$ext").exists()) { "$baseName-$source.$ext" } else { "$baseName.$ext" @@ -121,21 +128,10 @@ open class MigrateContentProject(val events: List, val storageArea: File) val storeFile = store.using(filename).resolveConflict() - CachedToStore( - cachedFile = cached, - storeFile = storeFile - ) + CachedToStore(cachedFile = cached, storeFile = storeFile) } } - - data class CachedToStore( - val cachedFile: File, - val storeFile: File - ) - - data class CachedToStoreLanguage( - val cts: CachedToStore, - val language: String - ) -} \ No newline at end of file + data class CachedToStore(val cachedFile: File, val storeFile: File) + data class CachedToStoreLanguage(val cts: CachedToStore, val language: String) +} diff --git a/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProjectTest.kt b/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProjectTest.kt new file mode 100644 index 00000000..e55f5a6f --- /dev/null +++ b/shared/common/src/test/kotlin/no/iktdev/mediaprocessing/shared/common/projection/MigrateContentProjectTest.kt @@ -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 + Så: + Skal pathen alltid ligge under storageArea + """ + ) + fun videoPathIsUnderStorageArea() { + val storage = tempDir() + + val events = listOf( + 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 + Så: + Skal projeksjonen bruke eksisterende mappe + """ + ) + fun usesExistingCollectionFolder() { + val storage = tempDir() + val existing = File(storage, "Breaking Bad") + existing.mkdirs() + + val events = listOf( + 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 + ) + } +}