diff --git a/core/model/src/main/kotlin/com/miruplay/tv/model/CloudDriveAutomation.kt b/core/model/src/main/kotlin/com/miruplay/tv/model/CloudDriveAutomation.kt index d91e942f..5fd82f7e 100644 --- a/core/model/src/main/kotlin/com/miruplay/tv/model/CloudDriveAutomation.kt +++ b/core/model/src/main/kotlin/com/miruplay/tv/model/CloudDriveAutomation.kt @@ -17,6 +17,18 @@ fun String.isDefaultCloudDriveWebDavEndpoint(): Boolean = URI(DEFAULT_CLOUD_DRIVE_ENDPOINT_URL).port }.getOrDefault(false) +fun webDavDirectoryWarmupChain(parentPath: String): List = + buildList { + add("") + var current = "" + parentPath.replace('\\', '/').trim('/').split('/') + .filter(String::isNotBlank) + .forEach { segment -> + current += "/$segment" + add(current) + } + } + @Serializable data class CloudDriveAutomationConfig( val endpointUrl: String = DEFAULT_CLOUD_DRIVE_ENDPOINT_URL, diff --git a/core/model/src/test/kotlin/com/miruplay/tv/model/CloudDriveDirectoryConventionsTest.kt b/core/model/src/test/kotlin/com/miruplay/tv/model/CloudDriveDirectoryConventionsTest.kt index 999d0536..1ffecc75 100644 --- a/core/model/src/test/kotlin/com/miruplay/tv/model/CloudDriveDirectoryConventionsTest.kt +++ b/core/model/src/test/kotlin/com/miruplay/tv/model/CloudDriveDirectoryConventionsTest.kt @@ -13,6 +13,15 @@ class CloudDriveDirectoryConventionsTest { assertEquals("/CloudRoot/Anime", normalizeCloudDriveDirectoryPath("/CloudRoot/Anime/")) } + @Test + fun `builds WebDAV warmup chain from root to parent`() { + assertEquals(listOf(""), webDavDirectoryWarmupChain("")) + assertEquals( + listOf("", "/115open", "/115open/影音", "/115open/影音/动漫"), + webDavDirectoryWarmupChain("/115open/影音/动漫/"), + ) + } + @Test fun `scopes navigation requests to token root`() { assertEquals("/CloudRoot", scopedCloudDriveDirectoryPath("/", "/CloudRoot")) diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolver.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolver.kt index 4aa13797..87887cbe 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolver.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolver.kt @@ -9,6 +9,7 @@ import com.miruplay.tv.model.connectionPassword import com.miruplay.tv.model.connectionUsername import com.miruplay.tv.model.isDefaultCloudDriveWebDavEndpoint import com.miruplay.tv.model.remoteUrl +import com.miruplay.tv.model.webDavDirectoryWarmupChain import com.miruplay.tv.repository.MediaSourceRepository import java.util.Base64 import javax.inject.Inject @@ -27,7 +28,7 @@ class PlaybackHttpRequestResolver @Inject constructor( if (remoteUrl.isBlank()) return PlaybackHttpRequestConfig.Empty if (remoteUrl.isDefaultCloudDriveWebDavEndpoint()) { - warmWebDavParentDirectory(mediaSource, source.uri, remoteUrl) + warmWebDavParentDirectories(mediaSource, source.uri, remoteUrl) } return PlaybackHttpRequestConfig( baseUrl = remoteUrl, @@ -35,15 +36,19 @@ class PlaybackHttpRequestResolver @Inject constructor( ) } - private suspend fun warmWebDavParentDirectory( + private suspend fun warmWebDavParentDirectories( source: MediaSourceInfo, uri: String, remoteUrl: String, ) { - val parentPath = webDavParentDirectoryForPlayback(uri, remoteUrl) ?: return + val directories = webDavParentDirectoryForPlayback(uri, remoteUrl) + ?.let(::webDavDirectoryWarmupChain) + ?: return val mediaSource = mediaSourceFactory?.create(source)?.getOrNull() ?: return try { - runCatching { mediaSource.listFiles(parentPath) } + directories.forEach { directory -> + runCatching { mediaSource.listFiles(directory) } + } } finally { runCatching { mediaSource.close() } } diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolverTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolverTest.kt index b4a36c0d..deacf7c6 100644 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolverTest.kt +++ b/player-core/src/test/kotlin/com/miruplay/tv/player/PlaybackHttpRequestResolverTest.kt @@ -10,7 +10,7 @@ import com.miruplay.tv.model.MediaSourceType import com.miruplay.tv.model.PlaybackSource import com.miruplay.tv.repository.MediaSourceRepository import io.mockk.coEvery -import io.mockk.coVerify +import io.mockk.coVerifySequence import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -71,10 +71,15 @@ class PlaybackHttpRequestResolverTest { ), ) - coVerify(exactly = 1) { + coVerifySequence { + webDavSource.listFiles("") + webDavSource.listFiles("/115open") + webDavSource.listFiles("/115open/影音") + webDavSource.listFiles("/115open/影音/动漫") + webDavSource.listFiles("/115open/影音/动漫/從 0 位居民開始的邊境領主大人") webDavSource.listFiles("/115open/影音/动漫/從 0 位居民開始的邊境領主大人/Season 1") + webDavSource.close() } - coVerify(exactly = 1) { webDavSource.close() } } @Test diff --git a/scanner/src/main/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporter.kt b/scanner/src/main/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporter.kt index 3c53ce8d..e296fec0 100644 --- a/scanner/src/main/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporter.kt +++ b/scanner/src/main/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporter.kt @@ -14,6 +14,7 @@ import com.miruplay.tv.model.MediaSourceType import com.miruplay.tv.model.Episode import com.miruplay.tv.model.isDefaultCloudDriveWebDavEndpoint import com.miruplay.tv.model.remoteUrl +import com.miruplay.tv.model.webDavDirectoryWarmupChain import com.miruplay.tv.repository.MediaExtraKind import com.miruplay.tv.repository.MediaIndexEntry import com.miruplay.tv.repository.MediaIndexRepository @@ -95,8 +96,8 @@ class MlipLibraryIndexImporter @Inject constructor( } } - val warmCloudDriveArtworkParents = - source.remoteUrl().orEmpty().isDefaultCloudDriveWebDavEndpoint() + val warmedCloudDriveArtworkDirectories = + if (source.remoteUrl().orEmpty().isDefaultCloudDriveWebDavEndpoint()) mutableSetOf() else null var artworkCachedCount = 0 for (series in snapshot.series) { val seriesFiles = mediaFiles.filter { it.seriesId == series.id } @@ -137,7 +138,7 @@ class MlipLibraryIndexImporter @Inject constructor( posterCacheDirectory = posterCacheDirectory, sourceId = source.id, seriesUuid = series.uuid, - warmParentDirectory = warmCloudDriveArtworkParents, + warmedDirectories = warmedCloudDriveArtworkDirectories, ) } ?.also { artworkCachedCount += 1 } @@ -570,7 +571,7 @@ class MlipLibraryIndexImporter @Inject constructor( posterCacheDirectory: File?, sourceId: Long, seriesUuid: String, - warmParentDirectory: Boolean, + warmedDirectories: MutableSet?, ): String? { val cacheRoot = posterCacheDirectory ?: return null val outputDir = File(cacheRoot, "mlip/$sourceId").apply { mkdirs() } @@ -580,10 +581,10 @@ class MlipLibraryIndexImporter @Inject constructor( ?: "jpg" val output = File(outputDir, "${stableHash("$seriesUuid:$artworkPath")}.$extension") if (output.length() > 0L) return output.absolutePath - if (warmParentDirectory) { - artworkPath.substringBeforeLast('/', "") - .takeIf(String::isNotBlank) - ?.let { parent -> runCatching { mediaSource.listFiles(parent) } } + if (warmedDirectories != null) { + webDavDirectoryWarmupChain(artworkPath.substringBeforeLast('/', "")) + .filter(warmedDirectories::add) + .forEach { directory -> runCatching { mediaSource.listFiles(directory) } } } val stream = mediaSource.openStream(artworkPath).getOrNull() ?: return null return runCatching { diff --git a/scanner/src/test/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporterTest.kt b/scanner/src/test/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporterTest.kt index 424104b5..fa0573a6 100644 --- a/scanner/src/test/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporterTest.kt +++ b/scanner/src/test/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporterTest.kt @@ -557,7 +557,7 @@ class MlipLibraryIndexImporterTest { assertTrue(result is Result.Success) assertEquals(1, (result as Result.Success).data.artworkCachedCount) - assertEquals(listOf("/Series"), mediaSource.listedPaths) + assertEquals(listOf("", "/Series"), mediaSource.listedPaths) assertNotNull(metadataRepository.anime.single().posterLocalPath) } finally { databaseFile.delete()