diff --git a/build.gradle.kts b/build.gradle.kts index 0c7bbdd..016f553 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") implementation(platform("software.amazon.awssdk:bom:2.46.21")) implementation("software.amazon.awssdk:s3") + implementation("com.squareup.okhttp3:okhttp:4.12.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation(kotlin("test-junit5")) diff --git a/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt b/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt index 03f0017..981bd2c 100644 --- a/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt +++ b/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt @@ -28,6 +28,7 @@ class InputService( logger.info("Download cancelled: {}", source) throw e } catch (e: Throwable) { + logger.warn("Download failed: {}", e.message, e) throw IllegalStateException("Can't download video from $source", e) } logger.info("Downloaded success in {} ms", (System.nanoTime() - startTime) / 1_000_000) diff --git a/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt b/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt index ae04841..9004b7d 100644 --- a/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt +++ b/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt @@ -1,15 +1,18 @@ package pw.binom.mirror.worker.convert import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Component -import java.net.HttpURLConnection -import java.net.URL +import java.io.IOException import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption +import java.util.concurrent.TimeUnit @Component class JellyfinInput( @@ -17,24 +20,46 @@ class JellyfinInput( ) : InputService.InputImplementation { private val logger: Logger = LoggerFactory.getLogger(JellyfinInput::class.java) + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .build() + } + override fun isSupport(source: InputSource): Boolean = source is InputSource.Jellyfin override suspend fun download(source: InputSource): Path = withContext(Dispatchers.IO) { source as InputSource.Jellyfin - val url = URL(source.streamUrl) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.instanceFollowRedirects = true - connection.connectTimeout = 30_000 - connection.readTimeout = 120_000 - try { - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw IllegalStateException("Invalid response code ${connection.responseCode}") + var lastError: IOException? = null + for (attempt in 1..3) { + try { + return@withContext downloadOnce(source) + } catch (e: IOException) { + lastError = e + logger.warn("Download attempt {}/3 failed: {}", attempt, e.message) + if (attempt < 3) { + delay(5_000) + } + } + } + throw lastError!! + } + + private fun downloadOnce(source: InputSource.Jellyfin): Path { + val request = Request.Builder().url(source.streamUrl).get().build() + val response = client.newCall(request).execute() + response.use { + if (!response.isSuccessful) { + throw IllegalStateException("Invalid response code ${response.code}") } val resultFile = localStorageService.genFile() - logger.info("Downloading file in to ${resultFile}...") - try { - connection.inputStream.use { input -> + logger.info("Downloading file in to {}...", resultFile) + val path = try { + val body = response.body ?: throw IllegalStateException("Empty response body") + body.byteStream().use { input -> Files.copy(input, resultFile, StandardCopyOption.REPLACE_EXISTING) } logger.info("File success downloaded!") @@ -43,8 +68,7 @@ class JellyfinInput( runCatching { Files.deleteIfExists(resultFile) } throw e } - } finally { - connection.disconnect() + return path } } }