Merge pull request #3957 from Bnyro/master

Significantly increase download speed using range requests
This commit is contained in:
Bnyro 2023-06-08 19:59:42 +02:00 committed by GitHub
commit 854ba87fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -59,6 +59,7 @@ import kotlin.io.path.absolute
import kotlin.io.path.createFile import kotlin.io.path.createFile
import kotlin.io.path.deleteIfExists import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.math.min
/** /**
* Download service with custom implementation of downloading using [HttpURLConnection]. * Download service with custom implementation of downloading using [HttpURLConnection].
@ -159,6 +160,7 @@ class DownloadService : LifecycleService() {
* Download file and emit [DownloadStatus] to the collectors of [downloadFlow] * Download file and emit [DownloadStatus] to the collectors of [downloadFlow]
* and notification. * and notification.
*/ */
@Suppress("KotlinConstantConditions")
private suspend fun downloadFile(item: DownloadItem) { private suspend fun downloadFile(item: DownloadItem) {
downloadQueue[item.id] = true downloadQueue[item.id] = true
val notificationBuilder = getNotificationBuilder(item) val notificationBuilder = getNotificationBuilder(item)
@ -169,104 +171,71 @@ class DownloadService : LifecycleService() {
// only fetch the content length if it's not been returned by the API // only fetch the content length if it's not been returned by the API
if (item.downloadSize == 0L) { if (item.downloadSize == 0L) {
url.getContentLength()?.takeIf { it != item.downloadSize }?.let { size -> url.getContentLength()?.let { size ->
item.downloadSize = size item.downloadSize = size
Database.downloadDao().updateDownloadItem(item) Database.downloadDao().updateDownloadItem(item)
} }
} }
try { while (totalRead < item.downloadSize) {
// Set start range where last downloading was held.
val con = CronetHelper.cronetEngine.openConnection(url) as HttpURLConnection
con.requestMethod = "GET"
con.setRequestProperty("Range", "bytes=$totalRead-")
con.connectTimeout = DownloadHelper.DEFAULT_TIMEOUT
con.readTimeout = DownloadHelper.DEFAULT_TIMEOUT
withContext(Dispatchers.IO) {
// Retry connecting to server for n times.
for (i in 1..DownloadHelper.DEFAULT_RETRY) {
try {
con.connect()
break
} catch (_: SocketTimeoutException) {
val message = getString(R.string.downloadfailed) + " " + i
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
toastFromMainThread(message)
}
}
}
// If link is expired try to regenerate using available info.
if (con.responseCode == 403) {
regenerateLink(item)
con.disconnect()
downloadFile(item)
return
} else if (con.responseCode !in 200..299) {
val message = getString(R.string.downloadfailed) + ": " + con.responseMessage
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
toastFromMainThread(message)
con.disconnect()
pause(item.id)
return
}
@Suppress("NewApi") // The StandardOpenOption enum is desugared.
val sink = path.sink(StandardOpenOption.APPEND).buffer()
val sourceByte = con.inputStream.source()
var lastTime = System.currentTimeMillis() / 1000
var lastRead: Long = 0
try { try {
// Check if downloading is still active and read next bytes. val con = startConnection(item, url, totalRead, item.downloadSize) ?: return
while (downloadQueue[item.id] && sourceByte
.read(sink.buffer, DownloadHelper.DOWNLOAD_CHUNK_SIZE)
.also { lastRead = it } != -1L
) {
sink.emit()
totalRead += lastRead
_downloadFlow.emit(
item.id to DownloadStatus.Progress(
lastRead,
totalRead,
item.downloadSize,
),
)
if (item.downloadSize != -1L &&
System.currentTimeMillis() / 1000 > lastTime
) {
notificationBuilder
.setContentText(
totalRead.formatAsFileSize() + " / " +
item.downloadSize.formatAsFileSize(),
)
.setProgress(
item.downloadSize.toInt(),
totalRead.toInt(),
false,
)
notificationManager.notify(
item.getNotificationId(),
notificationBuilder.build(),
)
lastTime = System.currentTimeMillis() / 1000
}
}
} catch (_: CancellationException) {
} catch (e: Exception) {
toastFromMainThread("${getString(R.string.download)}: ${e.message}")
_downloadFlow.emit(item.id to DownloadStatus.Error(e.message.toString(), e))
}
withContext(Dispatchers.IO) { @Suppress("NewApi") // The StandardOpenOption enum is desugared.
sink.flush() val sink = path.sink(StandardOpenOption.APPEND).buffer()
sink.close() val sourceByte = con.inputStream.source()
sourceByte.close()
con.disconnect() var lastTime = System.currentTimeMillis() / 1000
} var lastRead: Long = 0
} catch (_: Exception) {
try {
// Check if downloading is still active and read next bytes.
while (downloadQueue[item.id] && sourceByte
.read(sink.buffer, DownloadHelper.DOWNLOAD_CHUNK_SIZE)
.also { lastRead = it } != -1L
) {
sink.emit()
totalRead += lastRead
_downloadFlow.emit(
item.id to DownloadStatus.Progress(
lastRead,
totalRead,
item.downloadSize,
),
)
if (item.downloadSize != -1L &&
System.currentTimeMillis() / 1000 > lastTime
) {
notificationBuilder
.setContentText(
totalRead.formatAsFileSize() + " / " +
item.downloadSize.formatAsFileSize(),
)
.setProgress(
item.downloadSize.toInt(),
totalRead.toInt(),
false,
)
notificationManager.notify(
item.getNotificationId(),
notificationBuilder.build(),
)
lastTime = System.currentTimeMillis() / 1000
}
}
} catch (_: CancellationException) {
} catch (e: Exception) {
toastFromMainThread("${getString(R.string.download)}: ${e.message}")
_downloadFlow.emit(item.id to DownloadStatus.Error(e.message.toString(), e))
}
withContext(Dispatchers.IO) {
sink.flush()
sink.close()
sourceByte.close()
con.disconnect()
}
} catch (_: Exception) {}
} }
val completed = when { val completed = when {
@ -283,6 +252,56 @@ class DownloadService : LifecycleService() {
pause(item.id) pause(item.id)
} }
private suspend fun startConnection(
item: DownloadItem,
url: URL,
alreadyRead: Long,
readLimit: Long?
): HttpURLConnection? {
// Set start range where last downloading was held.
val con = CronetHelper.cronetEngine.openConnection(url) as HttpURLConnection
con.requestMethod = "GET"
val limit = if (readLimit == null) {
""
} else {
min(readLimit, alreadyRead + BYTES_PER_REQUEST)
}
con.setRequestProperty("Range", "bytes=$alreadyRead-$limit")
con.connectTimeout = DownloadHelper.DEFAULT_TIMEOUT
con.readTimeout = DownloadHelper.DEFAULT_TIMEOUT
withContext(Dispatchers.IO) {
// Retry connecting to server for n times.
for (i in 1..DownloadHelper.DEFAULT_RETRY) {
try {
con.connect()
break
} catch (_: SocketTimeoutException) {
val message = getString(R.string.downloadfailed) + " " + i
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
toastFromMainThread(message)
}
}
}
// If link is expired try to regenerate using available info.
if (con.responseCode == 403) {
regenerateLink(item)
con.disconnect()
downloadFile(item)
return null
} else if (con.responseCode !in 200..299) {
val message = getString(R.string.downloadfailed) + ": " + con.responseMessage
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
toastFromMainThread(message)
con.disconnect()
pause(item.id)
return null
}
return con
}
/** /**
* Resume download which may have been paused. * Resume download which may have been paused.
*/ */
@ -468,5 +487,6 @@ class DownloadService : LifecycleService() {
const val ACTION_SERVICE_STOPPED = const val ACTION_SERVICE_STOPPED =
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STOPPED" "com.github.libretube.services.DownloadService.ACTION_SERVICE_STOPPED"
var IS_DOWNLOAD_RUNNING = false var IS_DOWNLOAD_RUNNING = false
private const val BYTES_PER_REQUEST = 10000000L
} }
} }