wipp todo github releaseas in ytdlnis plugins repo

pull/1112/head
deniscerri 5 months ago
parent ab524a9509
commit 32a1c85d02
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -160,7 +160,7 @@ dependencies {
implementation "io.github.junkfood02.youtubedl-android:aria2c:0.18.1" 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: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 "io.github.junkfood02.youtubedl-android:aria2c:0.17.2"
implementation "androidx.appcompat:appcompat:1.7.1" implementation "androidx.appcompat:appcompat:1.7.1"

@ -198,6 +198,15 @@ object RuntimeManager {
request.addOption("--js-runtimes", "quickjs:${quickJsLocation.executable.absolutePath}") 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) { if (!usingCacheDir) {
request.addOption("--no-cache-dir") request.addOption("--no-cache-dir")
} }

@ -5,5 +5,6 @@ object Aria2c : PluginBase() {
override val pluginFolderName: String get() = "aria2c" override val pluginFolderName: String get() = "aria2c"
override val bundledZipName: String get() = "libaria2c.zip.so" override val bundledZipName: String get() = "libaria2c.zip.so"
override val bundledVersion: String get() = "v1.37.0" override val bundledVersion: String get() = "v1.37.0"
override val githubRepositoryPackageURL: String get() = "" override val packageGithubRepo: String get() = ""
override val githubPackageName: String get() = ""
} }

@ -4,6 +4,7 @@ object FFmpeg : PluginBase() {
override val executableName: String get() = "ffmpeg" override val executableName: String get() = "ffmpeg"
override val pluginFolderName: String get() = "ffmpeg" override val pluginFolderName: String get() = "ffmpeg"
override val bundledZipName: String get() = "libffmpeg.zip.so" override val bundledZipName: String get() = "libffmpeg.zip.so"
override val bundledVersion: String get() = "v7.1.1" override val bundledVersion: String get() = "v7.0.1"
override val githubRepositoryPackageURL: String get() = "" override val packageGithubRepo: String get() = "deniscerri/ytdlnis-packages"
override val githubPackageName: String get() = "ffmpeg"
} }

@ -4,6 +4,7 @@ object NodeJS : PluginBase() {
override val executableName: String get() = "node" override val executableName: String get() = "node"
override val pluginFolderName: String get() = "node" override val pluginFolderName: String get() = "node"
override val bundledZipName: String get() = "libnode.zip.so" override val bundledZipName: String get() = "libnode.zip.so"
override val bundledVersion: String get() = "v25.3.0" override val bundledVersion: String get() = ""
override val githubRepositoryPackageURL: String get() = "" override val packageGithubRepo: String get() = "deniscerri/ytdlnis-plugins"
override val githubPackageName: String get() = "nodejs"
} }

@ -9,6 +9,7 @@ import com.anggrayudi.storage.file.toRawFile
import com.deniscerri.ytdl.core.RuntimeManager import com.deniscerri.ytdl.core.RuntimeManager
import com.deniscerri.ytdl.core.ZipUtils import com.deniscerri.ytdl.core.ZipUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -18,26 +19,30 @@ import okhttp3.Request
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import java.io.File import java.io.File
import java.time.LocalDate import java.time.LocalDate
import kotlin.coroutines.cancellation.CancellationException
abstract class PluginBase { abstract class PluginBase {
protected abstract val executableName: String // e.g., "ffmpeg" protected abstract val executableName: String // e.g., "ffmpeg"
protected abstract val pluginFolderName: 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 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 fun getInstance(): PluginBase = this
abstract val bundledVersion: String? abstract val bundledVersion: String?
var downloadedVersion: String? = null var downloadedVersion: String? = null
protected abstract val githubRepositoryPackageURL: String // github repository package url
@Serializable @Serializable
data class PluginRelease( data class PluginRelease(
@SerialName("name") @SerialName("name")
val version: String, var version: String,
@SerialName("package_html_url")
val downloadUrl: String,
@SerialName("created_at") @SerialName("created_at")
val createdAt: String, 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( data class PluginLocation(
@ -131,19 +136,20 @@ abstract class PluginBase {
) )
} }
suspend fun downloadRelease(context: Context, release: PluginRelease, onProgress: (Int) -> Unit) : DocumentFile? { suspend fun downloadRelease(context: Context, release: PluginRelease, onProgress: (Int) -> Unit) : Result<DocumentFile> {
val runtimeDir = getDownloadedDir(context)
FileUtils.deleteQuietly(runtimeDir)
runtimeDir.mkdirs()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
val tempZipFile = File(context.cacheDir, "${pluginFolderName}_tmp.zip") val tempZipFile = File(context.cacheDir, "${pluginFolderName}_tmp.zip")
//download //download
val request = Request.Builder().url(release.downloadUrl).build() val request = Request.Builder()
.url(release.downloadUrl)
.build()
val response = sharedClient.newCall(request).execute() 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 body = response.body
val totalBytes = body.contentLength() val totalBytes = body.contentLength()
@ -155,6 +161,8 @@ abstract class PluginBase {
var bytesRead: Int var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) { while (inputStream.read(buffer).also { bytesRead = it } != -1) {
if (!isActive) throw CancellationException("Download cancelled by user")
outputStream.write(buffer, 0, bytesRead) outputStream.write(buffer, 0, bytesRead)
bytesDownloaded += bytesRead bytesDownloaded += bytesRead
@ -165,9 +173,9 @@ abstract class PluginBase {
} }
} }
DocumentFile.fromFile(tempZipFile) Result.success(DocumentFile.fromFile(tempZipFile))
} catch (e: Exception) { } catch (e: Exception) {
null Result.failure(e)
} }
} }
} }
@ -175,8 +183,9 @@ abstract class PluginBase {
fun installFromZip(context: Context, zipFile: DocumentFile, versionTag: String? = null) : Result<String> { fun installFromZip(context: Context, zipFile: DocumentFile, versionTag: String? = null) : Result<String> {
return kotlin.runCatching { return kotlin.runCatching {
val runtimeDir = getDownloadedDir(context) val runtimeDir = getDownloadedDir(context)
runtimeDir.deleteRecursively() FileUtils.deleteQuietly(runtimeDir)
runtimeDir.createNewFile() runtimeDir.mkdirs()
// 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so) // 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so)
context.contentResolver.openInputStream(zipFile.uri).use { inputStream -> context.contentResolver.openInputStream(zipFile.uri).use { inputStream ->
ZipUtils.unzip(inputStream, runtimeDir) ZipUtils.unzip(inputStream, runtimeDir)
@ -234,20 +243,10 @@ abstract class PluginBase {
} }
suspend fun getReleases() : List<PluginRelease> { suspend fun getReleases() : List<PluginRelease> {
// if (packageGithubRepo.isEmpty()) return listOf()
// 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()
val request = Request.Builder() 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") .header("Accept", "application/vnd.github+json")
.build() .build()
@ -260,9 +259,11 @@ abstract class PluginBase {
val supportedArch = getArchSuffix() val supportedArch = getArchSuffix()
json.decodeFromString<List<PluginRelease>>(jsonString) json.decodeFromString<List<PluginRelease>>(jsonString)
.filter { it.version.contains(supportedArch) }
.onEach { .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 { } else {
emptyList() emptyList()

@ -4,6 +4,7 @@ object Python : PluginBase() {
override val executableName: String get() = "python" override val executableName: String get() = "python"
override val pluginFolderName: String get() = "python" override val pluginFolderName: String get() = "python"
override val bundledZipName: String get() = "libpython.zip.so" override val bundledZipName: String get() = "libpython.zip.so"
override val bundledVersion: String get() = "v3.12.11" override val bundledVersion: String get() = "v3.12"
override val githubRepositoryPackageURL: String get() = "" override val packageGithubRepo: String get() = "deniscerri/ytdlnis-packages"
override val githubPackageName: String get() = "python"
} }

@ -5,5 +5,6 @@ object QuickJS : PluginBase() {
override val pluginFolderName: String get() = "quickjs" override val pluginFolderName: String get() = "quickjs"
override val bundledZipName: String get() = "libqjs.zip.so" override val bundledZipName: String get() = "libqjs.zip.so"
override val bundledVersion: String get() = "2025-04-26" override val bundledVersion: String get() = "2025-04-26"
override val githubRepositoryPackageURL: String get() = "" override val packageGithubRepo: String get() = ""
override val githubPackageName: String get() = ""
} }

@ -1,37 +1,24 @@
package com.deniscerri.ytdl.ui.adapter package com.deniscerri.ytdl.ui.adapter
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.view.isVisible
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.core.plugins.PluginBase
import com.deniscerri.ytdl.core.plugins.PluginBase.PluginRelease 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.button.MaterialButton
import com.google.android.material.chip.Chip import com.google.android.material.progressindicator.CircularProgressIndicator
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 java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Locale import java.util.Locale
import java.util.TimeZone
class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<PluginRelease?, PluginReleaseAdapter.ViewHolder>(AsyncDifferConfig.Builder( class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<PluginRelease?, PluginReleaseAdapter.ViewHolder>(AsyncDifferConfig.Builder(
DIFF_CALLBACK DIFF_CALLBACK
@ -65,26 +52,58 @@ class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: A
val item = getItem(position) ?: return val item = getItem(position) ?: return
val card = holder.itemView val card = holder.itemView
card.findViewById<TextView>(R.id.title).text = item.version card.findViewById<TextView>(R.id.title).text = "v${item.version}"
card.findViewById<TextView>(R.id.createdAt).text = item.createdAt
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<TextView>(R.id.createdAt).text = parser2.format(date.time)
val actionBtn = card.findViewById<MaterialButton>(R.id.actionBtn) val actionBtn = card.findViewById<MaterialButton>(R.id.actionBtn)
if (item.isInstalled) { actionBtn.isVisible = !item.isBundled && item.downloadProgress < 100
val progress = card.findViewById<CircularProgressIndicator>(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) actionBtn.setIconResource(R.drawable.ic_baseline_delete_outline_24)
} else { } else {
actionBtn.setIconResource(R.drawable.ic_down) actionBtn.setIconResource(R.drawable.ic_down)
} }
card.setOnClickListener { actionBtn.setOnClickListener {
if (item.isInstalled) { if (!item.isBundled) {
onItemClickListener.onDeleteReleaseClick(item) if (item.isDownloading && item.downloadProgress < 100) {
} else { onItemClickListener.onCancelDownloadReleaseClick(item)
onItemClickListener.onDownloadReleaseClick(item) } else if (item.isInstalled) {
onItemClickListener.onDeleteReleaseClick(item)
} else {
onItemClickListener.onDownloadReleaseClick(item)
}
} }
} }
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
val card = holder.itemView
val progress = card.findViewById<CircularProgressIndicator>(R.id.progress)
val progressValue = payloads.last().toString().toInt()
progress.progress = progressValue
progress.isIndeterminate = progressValue == 0 || progressValue == 100
}
}
interface OnItemClickListener { interface OnItemClickListener {
fun onCancelDownloadReleaseClick(item: PluginRelease)
fun onDownloadReleaseClick(item: PluginRelease) fun onDownloadReleaseClick(item: PluginRelease)
fun onDeleteReleaseClick(item: PluginRelease) fun onDeleteReleaseClick(item: PluginRelease)
} }
@ -96,7 +115,9 @@ class PluginReleaseAdapter(onItemClickListener: OnItemClickListener, activity: A
} }
override fun areContentsTheSame(oldItem: PluginRelease, newItem: PluginRelease): Boolean { 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
} }
} }
} }

@ -6,6 +6,7 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -39,6 +40,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import junit.runner.Version import junit.runner.Version
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -52,6 +54,7 @@ class PluginsFragment : Fragment(), PluginsAdapter.OnItemClickListener, PluginRe
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private var tmpItem: PluginItem? = null private var tmpItem: PluginItem? = null
private var tmpDownloadJob: Job? = null
private var plugins: List<PluginItem> = mutableListOf() private var plugins: List<PluginItem> = mutableListOf()
private var pluginReleases: List<PluginBase.PluginRelease> = mutableListOf() private var pluginReleases: List<PluginBase.PluginRelease> = mutableListOf()
@ -192,22 +195,61 @@ class PluginsFragment : Fragment(), PluginsAdapter.OnItemClickListener, PluginRe
deleteDownloadedVersion(item, currentVersion) deleteDownloadedVersion(item, currentVersion)
} }
override fun onDownloadReleaseClick(item: PluginBase.PluginRelease) { override fun onCancelDownloadReleaseClick(item: PluginBase.PluginRelease) {
lifecycleScope.launch { tmpDownloadJob?.cancel()
val instance = tmpItem!!.getInstance() 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) fileResp.onFailure {
resp.onFailure { lifecycleScope.launch {
Snackbar.make(requireView(), it.message ?: getString(R.string.errored), Snackbar.LENGTH_LONG).show() withContext(Dispatchers.Main) {
bottomSheet?.dismiss()
Snackbar.make(requireView(), it.message ?: getString(R.string.errored), Snackbar.LENGTH_LONG).show()
}
}
} }
resp.onSuccess { fileResp.onSuccess { file ->
bottomSheet?.dismiss() val resp = instance.installFromZip(requireContext(), file, "v${item.version}")
listAdapter.notifyDataSetChanged() resp.onFailure {
RuntimeManager.reInit(requireContext()) 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())
}
}
}
} }
} }
} }
} }

@ -883,9 +883,9 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
val aria2 = sharedPreferences.getBoolean("aria2", false) val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") ytDlRequest.addOption("--downloader", "libaria2c.so")
//request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") //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\"") //request.addOption("--external-downloader-args", "aria2c:\"--check-certificate=false\"")
} }

@ -91,7 +91,7 @@
android:clickable="false" android:clickable="false"
android:contentDescription="@string/preferred_download_type" android:contentDescription="@string/preferred_download_type"
android:minHeight="0dp" android:minHeight="0dp"
android:padding="0dp" android:paddingHorizontal="10dp"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:icon="@drawable/ic_down" app:icon="@drawable/ic_down"
app:iconTint="?attr/colorAccent" app:iconTint="?attr/colorAccent"
@ -99,6 +99,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

Loading…
Cancel
Save