diff --git a/app/build.gradle b/app/build.gradle index 36e5cfc7..d20e8632 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -160,7 +160,7 @@ dependencies { implementation "io.github.junkfood02.youtubedl-android:aria2c:0.18.1" // implementation "io.github.junkfood02.youtubedl-android:library:0.17.4" - implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.18.1" + implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.17.2" // implementation "io.github.junkfood02.youtubedl-android:aria2c:0.17.2" implementation "androidx.appcompat:appcompat:1.7.1" diff --git a/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt b/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt index 8dd11542..f28ee6b5 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/RuntimeManager.kt @@ -198,6 +198,15 @@ object RuntimeManager { request.addOption("--js-runtimes", "quickjs:${quickJsLocation.executable.absolutePath}") } + if (request.buildCommand().contains("libaria2c.so")) { + request + .addOption("--external-downloader-args", "aria2c:--summary-interval=1") + .addOption( + "--external-downloader-args", + "aria2c:--ca-certificate=$ENV_SSL_CERT_FILE" + ) + } + if (!usingCacheDir) { request.addOption("--no-cache-dir") } diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt index 2a5345ac..f00550b9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Aria2c.kt @@ -5,5 +5,6 @@ object Aria2c : PluginBase() { override val pluginFolderName: String get() = "aria2c" override val bundledZipName: String get() = "libaria2c.zip.so" override val bundledVersion: String get() = "v1.37.0" - override val githubRepositoryPackageURL: String get() = "" + override val packageGithubRepo: String get() = "" + override val githubPackageName: String get() = "" } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt index 7a839677..bc4559f6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/FFmpeg.kt @@ -4,6 +4,7 @@ object FFmpeg : PluginBase() { override val executableName: String get() = "ffmpeg" override val pluginFolderName: String get() = "ffmpeg" override val bundledZipName: String get() = "libffmpeg.zip.so" - override val bundledVersion: String get() = "v7.1.1" - override val githubRepositoryPackageURL: String get() = "" + override val bundledVersion: String get() = "v7.0.1" + override val packageGithubRepo: String get() = "deniscerri/ytdlnis-packages" + override val githubPackageName: String get() = "ffmpeg" } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt index c6dc4b31..b22c77bd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/NodeJS.kt @@ -4,6 +4,7 @@ object NodeJS : PluginBase() { override val executableName: String get() = "node" override val pluginFolderName: String get() = "node" override val bundledZipName: String get() = "libnode.zip.so" - override val bundledVersion: String get() = "v25.3.0" - override val githubRepositoryPackageURL: String get() = "" + override val bundledVersion: String get() = "" + override val packageGithubRepo: String get() = "deniscerri/ytdlnis-plugins" + override val githubPackageName: String get() = "nodejs" } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt index 49f08356..807b63a2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/PluginBase.kt @@ -9,6 +9,7 @@ import com.anggrayudi.storage.file.toRawFile import com.deniscerri.ytdl.core.RuntimeManager import com.deniscerri.ytdl.core.ZipUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -18,26 +19,30 @@ import okhttp3.Request import org.apache.commons.io.FileUtils import java.io.File import java.time.LocalDate +import kotlin.coroutines.cancellation.CancellationException abstract class PluginBase { protected abstract val executableName: String // e.g., "ffmpeg" protected abstract val pluginFolderName: String // e.g., "ffmpeg" protected abstract val bundledZipName: String // e.g., "libffmpeg.zip.so" + protected abstract val packageGithubRepo: String // e.g deniscerri/ytdlnis-plugins + protected abstract val githubPackageName: String // e.g ffmpeg fun getInstance(): PluginBase = this abstract val bundledVersion: String? var downloadedVersion: String? = null - protected abstract val githubRepositoryPackageURL: String // github repository package url @Serializable data class PluginRelease( @SerialName("name") - val version: String, - @SerialName("package_html_url") - val downloadUrl: String, + var version: String, @SerialName("created_at") val createdAt: String, - var isInstalled: Boolean + var downloadUrl: String = "", + var isInstalled: Boolean = false, + var isBundled: Boolean = false, + var isDownloading: Boolean = false, + var downloadProgress: Int = 0 ) data class PluginLocation( @@ -131,19 +136,20 @@ abstract class PluginBase { ) } - suspend fun downloadRelease(context: Context, release: PluginRelease, onProgress: (Int) -> Unit) : DocumentFile? { - val runtimeDir = getDownloadedDir(context) - FileUtils.deleteQuietly(runtimeDir) - runtimeDir.mkdirs() - + suspend fun downloadRelease(context: Context, release: PluginRelease, onProgress: (Int) -> Unit) : Result { return withContext(Dispatchers.IO) { try { val tempZipFile = File(context.cacheDir, "${pluginFolderName}_tmp.zip") //download - val request = Request.Builder().url(release.downloadUrl).build() + val request = Request.Builder() + .url(release.downloadUrl) + .build() + val response = sharedClient.newCall(request).execute() - if (!response.isSuccessful) return@withContext null + if (!response.isSuccessful) { + return@withContext Result.failure(Throwable(response.body.string())) + } val body = response.body val totalBytes = body.contentLength() @@ -155,6 +161,8 @@ abstract class PluginBase { var bytesRead: Int while (inputStream.read(buffer).also { bytesRead = it } != -1) { + if (!isActive) throw CancellationException("Download cancelled by user") + outputStream.write(buffer, 0, bytesRead) bytesDownloaded += bytesRead @@ -165,9 +173,9 @@ abstract class PluginBase { } } - DocumentFile.fromFile(tempZipFile) + Result.success(DocumentFile.fromFile(tempZipFile)) } catch (e: Exception) { - null + Result.failure(e) } } } @@ -175,8 +183,9 @@ abstract class PluginBase { fun installFromZip(context: Context, zipFile: DocumentFile, versionTag: String? = null) : Result { return kotlin.runCatching { val runtimeDir = getDownloadedDir(context) - runtimeDir.deleteRecursively() - runtimeDir.createNewFile() + FileUtils.deleteQuietly(runtimeDir) + runtimeDir.mkdirs() + // 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so) context.contentResolver.openInputStream(zipFile.uri).use { inputStream -> ZipUtils.unzip(inputStream, runtimeDir) @@ -234,20 +243,10 @@ abstract class PluginBase { } suspend fun getReleases() : List { -// -// return listOf( -// PluginRelease( -// version = "1.0", -// downloadUrl = "http://192.168.1.144:8080/x86_64/x86_64.zip", -// createdAt = LocalDate.now().toString(), -// isInstalled = downloadedVersion == "1.0" -// ) -// ) - - if (githubRepositoryPackageURL.isEmpty()) return listOf() + if (packageGithubRepo.isEmpty()) return listOf() val request = Request.Builder() - .url(githubRepositoryPackageURL) + .url("https://api.github.com/users/${packageGithubRepo.split("/").first()}/packages/maven/${githubPackageName}/versions") .header("Accept", "application/vnd.github+json") .build() @@ -260,9 +259,11 @@ abstract class PluginBase { val supportedArch = getArchSuffix() json.decodeFromString>(jsonString) - .filter { it.version.contains(supportedArch) } .onEach { - it.isInstalled = downloadedVersion == it.version + it.version = it.version + it.isInstalled = downloadedVersion == "v${it.version}" + it.isBundled = bundledVersion == "v${it.version}" + it.downloadUrl = "https://maven.pkg.github.com/${packageGithubRepo}/${githubPackageName}/${it.version}/${githubPackageName}-${it.version}-${supportedArch}.zip" } } else { emptyList() diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt index 5cd26f2e..831c314f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/Python.kt @@ -4,6 +4,7 @@ object Python : PluginBase() { override val executableName: String get() = "python" override val pluginFolderName: String get() = "python" override val bundledZipName: String get() = "libpython.zip.so" - override val bundledVersion: String get() = "v3.12.11" - override val githubRepositoryPackageURL: String get() = "" + override val bundledVersion: String get() = "v3.12" + override val packageGithubRepo: String get() = "deniscerri/ytdlnis-packages" + override val githubPackageName: String get() = "python" } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/plugins/QuickJS.kt b/app/src/main/java/com/deniscerri/ytdl/core/plugins/QuickJS.kt index 7452f46e..f6d71e0b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/plugins/QuickJS.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/plugins/QuickJS.kt @@ -5,5 +5,6 @@ object QuickJS : PluginBase() { override val pluginFolderName: String get() = "quickjs" override val bundledZipName: String get() = "libqjs.zip.so" override val bundledVersion: String get() = "2025-04-26" - override val githubRepositoryPackageURL: String get() = "" + override val packageGithubRepo: String get() = "" + override val githubPackageName: String get() = "" } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginReleaseAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginReleaseAdapter.kt index 97a511dc..f9a7c148 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginReleaseAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/PluginReleaseAdapter.kt @@ -1,37 +1,24 @@ package com.deniscerri.ytdl.ui.adapter import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Bundle import android.text.format.DateFormat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button import android.widget.LinearLayout import android.widget.TextView -import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdl.R -import com.deniscerri.ytdl.core.plugins.PluginBase import com.deniscerri.ytdl.core.plugins.PluginBase.PluginRelease -import com.deniscerri.ytdl.database.models.DownloadItem -import com.deniscerri.ytdl.database.models.GithubRelease -import com.deniscerri.ytdl.database.models.PluginItem import com.google.android.material.button.MaterialButton -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import io.noties.markwon.AbstractMarkwonPlugin -import io.noties.markwon.Markwon -import io.noties.markwon.MarkwonConfiguration -import java.sql.Date +import com.google.android.material.progressindicator.CircularProgressIndicator import java.text.SimpleDateFormat -import java.time.Instant import java.util.Locale +import java.util.TimeZone class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( DIFF_CALLBACK @@ -65,26 +52,58 @@ class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: A val item = getItem(position) ?: return val card = holder.itemView - card.findViewById(R.id.title).text = item.version - card.findViewById(R.id.createdAt).text = item.createdAt + card.findViewById(R.id.title).text = "v${item.version}" + + val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()) + parser.timeZone = TimeZone.getTimeZone("UTC") + val date = parser.parse(item.createdAt) + + val parser2 = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()) + card.findViewById(R.id.createdAt).text = parser2.format(date.time) val actionBtn = card.findViewById(R.id.actionBtn) - if (item.isInstalled) { + actionBtn.isVisible = !item.isBundled && item.downloadProgress < 100 + + val progress = card.findViewById(R.id.progress) + progress.isVisible = item.isDownloading + + if (item.isDownloading) { + actionBtn.setIconResource(R.drawable.ic_cancel) + } else if (item.isInstalled) { actionBtn.setIconResource(R.drawable.ic_baseline_delete_outline_24) } else { actionBtn.setIconResource(R.drawable.ic_down) } - card.setOnClickListener { - if (item.isInstalled) { - onItemClickListener.onDeleteReleaseClick(item) - } else { - onItemClickListener.onDownloadReleaseClick(item) + actionBtn.setOnClickListener { + if (!item.isBundled) { + if (item.isDownloading && item.downloadProgress < 100) { + onItemClickListener.onCancelDownloadReleaseClick(item) + } else if (item.isInstalled) { + onItemClickListener.onDeleteReleaseClick(item) + } else { + onItemClickListener.onDownloadReleaseClick(item) + } } } } + + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + val card = holder.itemView + val progress = card.findViewById(R.id.progress) + + val progressValue = payloads.last().toString().toInt() + progress.progress = progressValue + progress.isIndeterminate = progressValue == 0 || progressValue == 100 + } + } + interface OnItemClickListener { + fun onCancelDownloadReleaseClick(item: PluginRelease) fun onDownloadReleaseClick(item: PluginRelease) fun onDeleteReleaseClick(item: PluginRelease) } @@ -96,7 +115,9 @@ class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: A } override fun areContentsTheSame(oldItem: PluginRelease, newItem: PluginRelease): Boolean { - return oldItem.isInstalled == newItem.isInstalled + return oldItem.isInstalled == newItem.isInstalled && + oldItem.isDownloading == newItem.isDownloading && + oldItem.downloadProgress == newItem.downloadProgress } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt index 8ec2512a..6415e84f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PluginsFragment.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.util.DisplayMetrics +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -39,6 +40,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.snackbar.Snackbar import junit.runner.Version import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -52,6 +54,7 @@ class PluginsFragment : Fragment(), PluginsAdapter.OnItemClickListener, PluginRe private lateinit var preferences: SharedPreferences private var tmpItem: PluginItem? = null + private var tmpDownloadJob: Job? = null private var plugins: List = mutableListOf() private var pluginReleases: List = mutableListOf() @@ -192,22 +195,61 @@ class PluginsFragment : Fragment(), PluginsAdapter.OnItemClickListener, PluginRe deleteDownloadedVersion(item, currentVersion) } - override fun onDownloadReleaseClick(item: PluginBase.PluginRelease) { - lifecycleScope.launch { - val instance = tmpItem!!.getInstance() + override fun onCancelDownloadReleaseClick(item: PluginBase.PluginRelease) { + tmpDownloadJob?.cancel() + val idx = pluginReleases.indexOf(item) + val list = pluginReleases.toMutableList() + list[idx] = list[idx].copy(isDownloading = false) + releaseAdapter.submitList(list.toList()) + } - val file = instance.downloadRelease(requireContext(), item) { + override fun onDownloadReleaseClick(item: PluginBase.PluginRelease) { + val instance = tmpItem!!.getInstance() + val idx = pluginReleases.indexOf(item) + val list = pluginReleases.toMutableList() + list[idx] = list[idx].copy(isDownloading = true) + releaseAdapter.submitList(list.toList()) + + tmpDownloadJob = lifecycleScope.launch { + val fileResp = instance.downloadRelease(requireContext(), item) { progress -> + lifecycleScope.launch { + withContext(Dispatchers.Main) { + list[idx] = list[idx].copy(downloadProgress = progress) + releaseAdapter.submitList(list.toList()) + //releaseAdapter.notifyItemChanged(idx, progress.toString()) + } + } } - val resp = instance.installFromZip(requireContext(), file!!, item.version) - resp.onFailure { - Snackbar.make(requireView(), it.message ?: getString(R.string.errored), Snackbar.LENGTH_LONG).show() + fileResp.onFailure { + lifecycleScope.launch { + withContext(Dispatchers.Main) { + bottomSheet?.dismiss() + Snackbar.make(requireView(), it.message ?: getString(R.string.errored), Snackbar.LENGTH_LONG).show() + } + } } - resp.onSuccess { - bottomSheet?.dismiss() - listAdapter.notifyDataSetChanged() - RuntimeManager.reInit(requireContext()) + fileResp.onSuccess { file -> + val resp = instance.installFromZip(requireContext(), file, "v${item.version}") + resp.onFailure { + lifecycleScope.launch { + withContext(Dispatchers.Main) { + bottomSheet?.dismiss() + Snackbar.make(requireView(), it.message ?: getString(R.string.errored), Snackbar.LENGTH_LONG).show() + } + } + } + resp.onSuccess { + lifecycleScope.launch { + withContext(Dispatchers.Main) { + bottomSheet?.dismiss() + listAdapter.notifyDataSetChanged() + RuntimeManager.reInit(requireContext()) + } + } + } } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/ytdlp/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/ytdlp/YTDLPUtil.kt index 4340661a..74f8a011 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/ytdlp/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/ytdlp/YTDLPUtil.kt @@ -883,9 +883,9 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co val aria2 = sharedPreferences.getBoolean("aria2", false) if (aria2) { - request.addOption("--downloader", "libaria2c.so") + ytDlRequest.addOption("--downloader", "libaria2c.so") //request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") - request.addOption("--no-check-certificates") + ytDlRequest.addOption("--no-check-certificates") //request.addOption("--external-downloader-args", "aria2c:\"--check-certificate=false\"") } diff --git a/app/src/main/res/layout/plugin_release_item.xml b/app/src/main/res/layout/plugin_release_item.xml index 28f7b900..03bca160 100644 --- a/app/src/main/res/layout/plugin_release_item.xml +++ b/app/src/main/res/layout/plugin_release_item.xml @@ -91,7 +91,7 @@ android:clickable="false" android:contentDescription="@string/preferred_download_type" android:minHeight="0dp" - android:padding="0dp" + android:paddingHorizontal="10dp" app:cornerRadius="10dp" app:icon="@drawable/ic_down" app:iconTint="?attr/colorAccent" @@ -99,6 +99,16 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> + +