mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
Merge pull request #3957 from Bnyro/master
Significantly increase download speed using range requests
This commit is contained in:
commit
854ba87fc5
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user