All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xyz.cssxsh.pixiv.tool.PixivDownloader.kt Maven / Gradle / Ivy

package xyz.cssxsh.pixiv.tool

import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.compression.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.utils.io.errors.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import okhttp3.ConnectionPool
import xyz.cssxsh.pixiv.*
import xyz.cssxsh.pixiv.exception.*
import java.net.*
import java.util.concurrent.*

public open class PixivDownloader(
    async: Int = 32,
    protected open val blockSize: Int = 512 * HTTP_KILO, // HTTP use 1022 no 1024,
    protected open val proxy: Proxy? = null,
    protected open val doh: String = JAPAN_DNS,
    protected open val host: Map> = DEFAULT_PIXIV_HOST,
) {

    protected open val timeout: Long = 10 * 1000L

    protected open val ignore: suspend (Throwable) -> Boolean = { it is IOException }

    protected open val channel: Channel = Channel(async)

    protected open fun client(): HttpClient = HttpClient(OkHttp) {
        ContentEncoding()
        install(HttpTimeout) {
            socketTimeoutMillis = timeout
            connectTimeoutMillis = timeout
            requestTimeoutMillis = null
        }
        defaultRequest {
            header(HttpHeaders.CacheControl, "no-cache")
            header(HttpHeaders.Connection, "keep-alive")
            header(HttpHeaders.Pragma, "no-cache")
        }
        expectSuccess = true
        engine {
            config {
                connectionPool(ConnectionPool(5, 10, TimeUnit.MINUTES))
                sslSocketFactory(RubySSLSocketFactory, RubyX509TrustManager)
                hostnameVerifier { _, _ -> true }
                proxy([email protected])
                dns(RubyDns(doh, host))
            }
        }
    }

    protected open val clients: MutableList by lazy { MutableList(8) { client() } }

    private suspend fun  withHttpClient(client: HttpClient, block: suspend HttpClient.() -> T): T = supervisorScope {
        while (isActive) {
            channel.send(clients.indexOf(client))
            return@supervisorScope try {
                with(client) { block() }
            } catch (throwable: Throwable) {
                if (isActive && ignore(throwable)) {
                    null
                } else {
                    throw throwable
                }
            } finally {
                channel.receive()
            } ?: continue
        }
        throw CancellationException(null, null)
    }

    private suspend fun length(client: HttpClient, url: Url): Int = withHttpClient(client) {
        val response = head(url) {
            header(HttpHeaders.Host, url.host)
            header(HttpHeaders.Referrer, url)
            header(HttpHeaders.Range, "bytes=0-")
            header(HttpHeaders.CacheControl, "no-store")
            header(HttpHeaders.Connection, "keep-alive")
            header(HttpHeaders.Pragma, "no-store")
        }
        if (response.headers[HttpHeaders.Age]?.toIntOrNull() == 0) {
            throw NoCacheException(response)
        }
        response.headers[HttpHeaders.ContentLength]?.toIntOrNull()
            ?: response.headers[HttpHeaders.ContentRange]?.substringAfter('/')?.toInt()
            ?: throw MatchContentLengthException(response)
    }

    private suspend fun range(client: HttpClient, url: Url, dst: ByteArray, offset: Int) = withHttpClient(client) {
        val length = minOf(dst.size - offset, blockSize)
        val range = "bytes=${offset}-${offset + length - 1}"

        val response = get(url) {
            header(HttpHeaders.Host, url.host)
            header(HttpHeaders.Referrer, url)
            header(HttpHeaders.Range, range)
            url {
                fragment = range
            }
        }

        if (response.headers[HttpHeaders.Age]?.toIntOrNull() == 0) {
            throw NoCacheException(response)
        }

        if ((response.headers[HttpHeaders.ContentLength]?.toIntOrNull() ?: -1) != length) {
            throw MatchContentLengthException(response)
        }

        response.bodyAsChannel().readFully(dst, offset, length)
    }

    private suspend fun all(client: HttpClient, url: Url, dst: ByteArray) = withHttpClient(client) {
        val response = get(url) {
            header(HttpHeaders.Host, url.host)
            header(HttpHeaders.Referrer, url)
        }

        if (response.headers[HttpHeaders.Age]?.toIntOrNull() == 0) {
            throw NoCacheException(response)
        }

        if ((response.headers[HttpHeaders.ContentLength]?.toIntOrNull() ?: -1) != dst.size) {
            throw MatchContentLengthException(response)
        }

        response.bodyAsChannel().readFully(dst, 0, dst.size)
    }

    private suspend fun downloadRangesOrAll(client: HttpClient, url: Url, length: Int): ByteArray = supervisorScope {
        val bytes = ByteArray(size = length)
        if (blockSize <= 0 || length <= blockSize) {
            all(client = client, url = url, dst = bytes)
        } else {
            (0 until length step blockSize).map { offset ->
                async(Dispatchers.IO) {
                    range(
                        client = client,
                        url = url,
                        dst = bytes,
                        offset = offset
                    )
                }
            }.awaitAll()
        }
        bytes
    }

    public open suspend fun download(url: Url): ByteArray = supervisorScope {
        var client = clients.random()
        var length = 0
        while (isActive && length == 0) {
            try {
                length = length(client = client, url = url)
            } catch (_: MatchContentLengthException) {
                client = clients.random()
            } catch (_: NoCacheException) {
                client = clients.random()
            }
        }

        downloadRangesOrAll(client = client, url = url, length = length)
    }

    public open suspend fun  downloadImageUrls(
        urls: List,
        block: suspend (url: Url, deferred: Deferred) -> R,
    ): List = supervisorScope {
        urls.map { url ->
            url to async { download(url) }
        }.map { (url, deferred) ->
            block(url, deferred)
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy