mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
Use java.nio.file APIs in download functionality.
This commit is contained in:
parent
9c237861c5
commit
13f8b3d49d
@ -2,6 +2,7 @@ package com.github.libretube.api.obj
|
||||
|
||||
import com.github.libretube.db.obj.DownloadItem
|
||||
import com.github.libretube.enums.FileType
|
||||
import java.nio.file.Paths
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@ -33,6 +34,7 @@ data class Streams(
|
||||
val uploaderSubscriberCount: Long = 0,
|
||||
val previewFrames: List<PreviewFrames> = emptyList()
|
||||
) {
|
||||
@Suppress("NewApi") // The Paths class is desugared.
|
||||
fun toDownloadItems(
|
||||
videoId: String,
|
||||
fileName: String,
|
||||
@ -53,7 +55,7 @@ data class Streams(
|
||||
type = FileType.VIDEO,
|
||||
videoId = videoId,
|
||||
fileName = stream?.getQualityString(fileName).orEmpty(),
|
||||
path = "",
|
||||
path = Paths.get(""),
|
||||
url = stream?.url,
|
||||
format = videoFormat,
|
||||
quality = videoQuality
|
||||
@ -70,7 +72,7 @@ data class Streams(
|
||||
type = FileType.AUDIO,
|
||||
videoId = videoId,
|
||||
fileName = stream?.getQualityString(fileName).orEmpty(),
|
||||
path = "",
|
||||
path = Paths.get(""),
|
||||
url = stream?.url,
|
||||
format = audioFormat,
|
||||
quality = audioQuality
|
||||
@ -84,7 +86,7 @@ data class Streams(
|
||||
type = FileType.SUBTITLE,
|
||||
videoId = videoId,
|
||||
fileName = "${fileName}_$subtitleCode.srt",
|
||||
path = "",
|
||||
path = Paths.get(""),
|
||||
url = subtitles.find { it.code == subtitleCode }?.url
|
||||
)
|
||||
)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.github.libretube.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.toLocalDate
|
||||
|
||||
@ -10,4 +12,10 @@ object Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun stringToLocalDate(string: String?) = string?.toLocalDate()
|
||||
|
||||
@TypeConverter
|
||||
fun pathToString(path: Path?) = path?.toString()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToPath(string: String?) = string?.let { Paths.get(it) }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package com.github.libretube.db.obj
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.nio.file.Path
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
@Entity(tableName = "download")
|
||||
@ -12,5 +13,5 @@ data class Download(
|
||||
val description: String = "",
|
||||
val uploader: String = "",
|
||||
val uploadDate: LocalDate? = null,
|
||||
val thumbnailPath: String? = null
|
||||
val thumbnailPath: Path? = null
|
||||
)
|
||||
|
@ -5,6 +5,7 @@ import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.libretube.enums.FileType
|
||||
import java.nio.file.Path
|
||||
|
||||
@Entity(
|
||||
tableName = "downloadItem",
|
||||
@ -24,7 +25,7 @@ data class DownloadItem(
|
||||
val type: FileType,
|
||||
val videoId: String,
|
||||
val fileName: String,
|
||||
var path: String,
|
||||
var path: Path,
|
||||
var url: String? = null,
|
||||
var format: String? = null,
|
||||
var quality: String? = null,
|
||||
|
11
app/src/main/java/com/github/libretube/extensions/Path.kt
Normal file
11
app/src/main/java/com/github/libretube/extensions/Path.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package com.github.libretube.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
|
||||
fun Path.toAndroidUri(): Uri? {
|
||||
@Suppress("NewApi") // The Path class is desugared.
|
||||
return if (exists()) toFile().toUri() else null
|
||||
}
|
@ -8,7 +8,8 @@ import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.db.obj.DownloadItem
|
||||
import com.github.libretube.services.DownloadService
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createDirectories
|
||||
|
||||
object DownloadHelper {
|
||||
const val VIDEO_DIR = "video"
|
||||
@ -20,23 +21,21 @@ object DownloadHelper {
|
||||
const val DEFAULT_TIMEOUT = 15 * 1000
|
||||
const val DEFAULT_RETRY = 3
|
||||
|
||||
fun getOfflineStorageDir(context: Context): File {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return context.filesDir
|
||||
|
||||
return try {
|
||||
context.getExternalFilesDir(null)!!
|
||||
} catch (e: Exception) {
|
||||
private fun getOfflineStorageDir(context: Context): Path {
|
||||
val file = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
context.filesDir
|
||||
} else {
|
||||
try {
|
||||
context.getExternalFilesDir(null)!!
|
||||
} catch (e: Exception) {
|
||||
context.filesDir
|
||||
}
|
||||
}
|
||||
return file.toPath()
|
||||
}
|
||||
|
||||
fun getDownloadDir(context: Context, path: String): File {
|
||||
return File(
|
||||
getOfflineStorageDir(context),
|
||||
path
|
||||
).apply {
|
||||
if (!this.exists()) this.mkdirs()
|
||||
}
|
||||
fun getDownloadDir(context: Context, path: String): Path {
|
||||
return getOfflineStorageDir(context).resolve(path).createDirectories()
|
||||
}
|
||||
|
||||
fun getMaxConcurrentDownloads(): Int {
|
||||
|
@ -6,6 +6,7 @@ import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.load
|
||||
@ -13,8 +14,9 @@ import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.github.libretube.api.CronetHelper
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.extensions.toAndroidUri
|
||||
import com.github.libretube.util.DataSaverMode
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
object ImageHelper {
|
||||
lateinit var imageLoader: ImageLoader
|
||||
@ -54,9 +56,10 @@ object ImageHelper {
|
||||
if (!DataSaverMode.isEnabled(target.context)) target.load(url, imageLoader)
|
||||
}
|
||||
|
||||
fun downloadImage(context: Context, url: String, path: String) {
|
||||
fun downloadImage(context: Context, url: String, path: Path) {
|
||||
getAsync(context, url) { bitmap ->
|
||||
saveImage(context, bitmap, Uri.fromFile(File(path)))
|
||||
@Suppress("NewApi") // The Path class is desugared.
|
||||
saveImage(context, bitmap, path.toFile().toUri())
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,10 +72,8 @@ object ImageHelper {
|
||||
imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
fun getDownloadedImage(context: Context, path: String): Bitmap? {
|
||||
val file = File(path)
|
||||
if (!file.exists()) return null
|
||||
return getImage(context, Uri.fromFile(file))
|
||||
fun getDownloadedImage(context: Context, path: Path): Bitmap? {
|
||||
return path.toAndroidUri()?.let { getImage(context, it) }
|
||||
}
|
||||
|
||||
private fun saveImage(context: Context, bitmapImage: Bitmap, imagePath: Uri) {
|
||||
|
@ -38,7 +38,12 @@ import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -48,7 +53,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.BufferedSink
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
@ -96,7 +100,8 @@ class DownloadService : LifecycleService() {
|
||||
RetrofitInstance.api.getStreams(videoId)
|
||||
}
|
||||
|
||||
val thumbnailTargetFile = getDownloadFile(DownloadHelper.THUMBNAIL_DIR, fileName)
|
||||
val thumbnailTargetPath = getDownloadPath(DownloadHelper.THUMBNAIL_DIR, fileName)
|
||||
.absolute()
|
||||
|
||||
val download = Download(
|
||||
videoId,
|
||||
@ -104,13 +109,13 @@ class DownloadService : LifecycleService() {
|
||||
streams.description,
|
||||
streams.uploader,
|
||||
streams.uploadDate,
|
||||
thumbnailTargetFile.absolutePath
|
||||
thumbnailTargetPath
|
||||
)
|
||||
Database.downloadDao().insertDownload(download)
|
||||
ImageHelper.downloadImage(
|
||||
this@DownloadService,
|
||||
streams.thumbnailUrl,
|
||||
thumbnailTargetFile.absolutePath
|
||||
thumbnailTargetPath
|
||||
)
|
||||
|
||||
val downloadItems = streams.toDownloadItems(
|
||||
@ -136,13 +141,11 @@ class DownloadService : LifecycleService() {
|
||||
* for the requested file.
|
||||
*/
|
||||
private fun start(item: DownloadItem) {
|
||||
val file = when (item.type) {
|
||||
FileType.AUDIO -> getDownloadFile(DownloadHelper.AUDIO_DIR, item.fileName)
|
||||
FileType.VIDEO -> getDownloadFile(DownloadHelper.VIDEO_DIR, item.fileName)
|
||||
FileType.SUBTITLE -> getDownloadFile(DownloadHelper.SUBTITLE_DIR, item.fileName)
|
||||
}
|
||||
file.createNewFile()
|
||||
item.path = file.absolutePath
|
||||
item.path = when (item.type) {
|
||||
FileType.AUDIO -> getDownloadPath(DownloadHelper.AUDIO_DIR, item.fileName)
|
||||
FileType.VIDEO -> getDownloadPath(DownloadHelper.VIDEO_DIR, item.fileName)
|
||||
FileType.SUBTITLE -> getDownloadPath(DownloadHelper.SUBTITLE_DIR, item.fileName)
|
||||
}.createFile().absolute()
|
||||
|
||||
lifecycleScope.launch(coroutineContext) {
|
||||
item.id = Database.downloadDao().insertDownloadItem(item).toInt()
|
||||
@ -158,8 +161,8 @@ class DownloadService : LifecycleService() {
|
||||
downloadQueue[item.id] = true
|
||||
val notificationBuilder = getNotificationBuilder(item)
|
||||
setResumeNotification(notificationBuilder, item)
|
||||
val file = File(item.path)
|
||||
var totalRead = file.length()
|
||||
val path = item.path
|
||||
var totalRead = path.fileSize()
|
||||
val url = URL(item.url ?: return)
|
||||
|
||||
url.getContentLength().let { size ->
|
||||
@ -206,7 +209,8 @@ class DownloadService : LifecycleService() {
|
||||
return
|
||||
}
|
||||
|
||||
val sink: BufferedSink = file.sink(true).buffer()
|
||||
@Suppress("NewApi") // The StandardOpenOption enum is desugared.
|
||||
val sink = path.sink(StandardOpenOption.APPEND).buffer()
|
||||
val sourceByte = con.inputStream.source()
|
||||
|
||||
var lastTime = System.currentTimeMillis() / 1000
|
||||
@ -437,14 +441,8 @@ class DownloadService : LifecycleService() {
|
||||
/**
|
||||
* Get a [File] from the corresponding download directory and the file name
|
||||
*/
|
||||
private fun getDownloadFile(directory: String, fileName: String): File {
|
||||
return File(
|
||||
DownloadHelper.getDownloadDir(
|
||||
this@DownloadService,
|
||||
directory
|
||||
),
|
||||
fileName
|
||||
)
|
||||
private fun getDownloadPath(directory: String, fileName: String): Path {
|
||||
return DownloadHelper.getDownloadDir(this, directory).resolve(fileName)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -15,6 +15,7 @@ import com.github.libretube.databinding.ActivityOfflinePlayerBinding
|
||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
import com.github.libretube.enums.FileType
|
||||
import com.github.libretube.extensions.toAndroidUri
|
||||
import com.github.libretube.extensions.updateParameters
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
|
||||
@ -33,7 +34,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -107,10 +107,6 @@ class OfflinePlayerActivity : BaseActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun File.toUri(): Uri? {
|
||||
return if (this.exists()) Uri.fromFile(this) else null
|
||||
}
|
||||
|
||||
private fun playVideo() {
|
||||
lifecycleScope.launch {
|
||||
val downloadFiles = withContext(Dispatchers.IO) {
|
||||
@ -121,9 +117,9 @@ class OfflinePlayerActivity : BaseActivity() {
|
||||
val audio = downloadFiles.firstOrNull { it.type == FileType.AUDIO }
|
||||
val subtitle = downloadFiles.firstOrNull { it.type == FileType.SUBTITLE }
|
||||
|
||||
val videoUri = video?.path?.let { File(it).toUri() }
|
||||
val audioUri = audio?.path?.let { File(it).toUri() }
|
||||
val subtitleUri = subtitle?.path?.let { File(it).toUri() }
|
||||
val videoUri = video?.path?.toAndroidUri()
|
||||
val audioUri = audio?.path?.toAndroidUri()
|
||||
val subtitleUri = subtitle?.path?.toAndroidUri()
|
||||
|
||||
setMediaSource(videoUri, audioUri, subtitleUri)
|
||||
|
||||
@ -143,7 +139,6 @@ class OfflinePlayerActivity : BaseActivity() {
|
||||
.setMimeType(MimeTypes.APPLICATION_SUBRIP)
|
||||
.build()
|
||||
}
|
||||
subtitle?.id
|
||||
|
||||
when {
|
||||
videoUri != null && audioUri != null -> {
|
||||
|
@ -18,7 +18,8 @@ import com.github.libretube.ui.activities.OfflinePlayerActivity
|
||||
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
||||
import com.github.libretube.util.TextUtils
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.File
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@ -46,7 +47,7 @@ class DownloadsAdapter(
|
||||
videoInfo.text = download.uploadDate?.let { TextUtils.localizeDate(it) }
|
||||
|
||||
val downloadSize = items.sumOf { it.downloadSize }
|
||||
val currentSize = items.sumOf { File(it.path).length() }
|
||||
val currentSize = items.sumOf { it.path.fileSize() }
|
||||
|
||||
if (downloadSize == -1L) {
|
||||
progressBar.isIndeterminate = true
|
||||
@ -96,16 +97,10 @@ class DownloadsAdapter(
|
||||
.setTitle(R.string.delete)
|
||||
.setMessage(R.string.irreversible)
|
||||
.setPositiveButton(R.string.okay) { _, _ ->
|
||||
items.map { File(it.path) }.forEach { file ->
|
||||
runCatching {
|
||||
if (file.exists()) file.delete()
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
download.thumbnailPath?.let {
|
||||
File(it).delete()
|
||||
}
|
||||
items.forEach {
|
||||
it.path.deleteIfExists()
|
||||
}
|
||||
download.thumbnailPath?.deleteIfExists()
|
||||
|
||||
runBlocking(Dispatchers.IO) {
|
||||
DatabaseHolder.Database.downloadDao().deleteDownload(download)
|
||||
|
@ -24,7 +24,7 @@ import com.github.libretube.receivers.DownloadReceiver
|
||||
import com.github.libretube.services.DownloadService
|
||||
import com.github.libretube.ui.adapters.DownloadsAdapter
|
||||
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
||||
import java.io.File
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@ -84,7 +84,7 @@ class DownloadsFragment : Fragment() {
|
||||
binding.downloads.adapter = DownloadsAdapter(requireContext(), downloads) {
|
||||
var isDownloading = false
|
||||
val ids = it.downloadItems
|
||||
.filter { item -> File(item.path).length() < item.downloadSize }
|
||||
.filter { item -> item.path.fileSize() < item.downloadSize }
|
||||
.map { item -> item.id }
|
||||
|
||||
if (!serviceConnection.isBound) {
|
||||
|
@ -36,7 +36,7 @@ androidx-work-runtime = { group = "androidx.work", name="work-runtime-ktx", vers
|
||||
exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" }
|
||||
exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
|
||||
square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" }
|
||||
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugaring" }
|
||||
exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
|
||||
exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" }
|
||||
cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user