success build nodejs, todo downloadable plugins
This commit is contained in:
+2
-1
@@ -8,4 +8,5 @@
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
/benchmark
|
||||
*.png
|
||||
*.png
|
||||
app/src/main/jniLibs
|
||||
@@ -103,6 +103,11 @@ object RuntimeManager {
|
||||
initialized = true
|
||||
}
|
||||
|
||||
fun reInit(context: Context) {
|
||||
initialized = false
|
||||
init(context)
|
||||
}
|
||||
|
||||
private fun assertInit() {
|
||||
check(initialized) { "instance not initialized" }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.deniscerri.ytdl.core.ZipUtils
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
abstract class BaseRuntime {
|
||||
@@ -51,30 +52,92 @@ abstract class BaseRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAndInstall(context: Context, zipUrl: String, versionTag: String) {
|
||||
val downloadDir = File(context.noBackupFilesDir, "runtimes/$runtimeName")
|
||||
FileUtils.deleteQuietly(downloadDir)
|
||||
downloadDir.mkdirs()
|
||||
fun downloadAndInstall(context: Context, zipUrl: String, versionTag: String, onProgress: (Long, Long) -> Unit) : Boolean {
|
||||
// 1. Clean up old installation
|
||||
val runtimeDir = File(context.noBackupFilesDir, "runtimes/$runtimeName")
|
||||
FileUtils.deleteQuietly(runtimeDir)
|
||||
runtimeDir.mkdirs()
|
||||
|
||||
val tempZip = File(context.cacheDir, "${runtimeName}_tmp.zip")
|
||||
var success = false
|
||||
|
||||
val tempZip = File(context.cacheDir, "${runtimeName}_bundle_tmp.zip")
|
||||
try {
|
||||
URL(zipUrl).openStream().use { input ->
|
||||
tempZip.outputStream().use { output -> input.copyTo(output) }
|
||||
// 2. Download the bundle (handles GitHub 302 redirects)
|
||||
downloadWithRedirects(zipUrl, tempZip, onProgress)
|
||||
// 3. Unzip the main Bundle (contains libnode.so and libnode.zip.so)
|
||||
ZipUtils.unzip(tempZip, runtimeDir)
|
||||
// 4. Handle the Bootstrap Zip (Double Unzip)
|
||||
// Look for any .zip.so file in the extracted directory
|
||||
runtimeDir.listFiles()?.forEach { file ->
|
||||
if (file.name.endsWith(".zip.so")) {
|
||||
ZipUtils.unzip(file, runtimeDir)
|
||||
file.delete() // Remove the internal zip to save space
|
||||
}
|
||||
}
|
||||
// 5. Global Permission Fix
|
||||
// Scan for all files in any 'bin' folder and make them executable
|
||||
applyExecutablePermissions(runtimeDir)
|
||||
// 6. Save installation state
|
||||
saveState(context, versionTag)
|
||||
success = true
|
||||
} catch (ex: Exception) {
|
||||
success = false
|
||||
}
|
||||
if (tempZip.exists()) tempZip.delete()
|
||||
return success
|
||||
}
|
||||
|
||||
ZipUtils.unzip(tempZip, downloadDir)
|
||||
private fun downloadWithRedirects(url: String, dest: File, onProgress: (Long, Long) -> Unit) {
|
||||
var currentUrl = url
|
||||
var connection: HttpURLConnection
|
||||
var redirectCount = 0
|
||||
|
||||
// Recursive function to set executable permissions on everything in bin/
|
||||
File(downloadDir, "bin").listFiles()?.forEach {
|
||||
it.setExecutable(true, false)
|
||||
while (true) {
|
||||
connection = URL(currentUrl).openConnection() as HttpURLConnection
|
||||
connection.instanceFollowRedirects = true
|
||||
|
||||
val status = connection.responseCode
|
||||
if (status == HttpURLConnection.HTTP_MOVED_TEMP ||
|
||||
status == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
status == HttpURLConnection.HTTP_SEE_OTHER) {
|
||||
|
||||
currentUrl = connection.getHeaderField("Location")
|
||||
redirectCount++
|
||||
if (redirectCount > 5) throw Exception("Too many redirects")
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(installedKey, true)
|
||||
putString(versionKey, versionTag)
|
||||
val fileSize = connection.contentLength.toLong()
|
||||
var bytesCopied = 0L
|
||||
val buffer = ByteArray(8192)
|
||||
|
||||
connection.inputStream.use { input ->
|
||||
dest.outputStream().use { output ->
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
output.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
// Trigger the callback
|
||||
onProgress?.invoke(bytesCopied, fileSize)
|
||||
bytes = input.read(buffer)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
tempZip.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyExecutablePermissions(file: File) {
|
||||
if (file.isDirectory) {
|
||||
// Check if this folder is a 'bin' folder
|
||||
if (file.name == "bin") {
|
||||
file.listFiles()?.forEach { it.setExecutable(true, false) }
|
||||
}
|
||||
// Recurse into subdirectories
|
||||
file.listFiles()?.forEach { applyExecutablePermissions(it) }
|
||||
} else if (file.name == "libnode.so") {
|
||||
// Specifically ensure our renamed main binary is executable
|
||||
file.setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +150,14 @@ abstract class BaseRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveState(context: Context, version: String) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit(commit = true) {
|
||||
putBoolean(installedKey, true)
|
||||
putString(versionKey, version)
|
||||
}
|
||||
}
|
||||
|
||||
open fun checkForUpdates(context: Context): RuntimeUpdateInfo? {
|
||||
return try {
|
||||
val response = URL(manifestURL).readText()
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.deniscerri.ytdl.core.runtimes
|
||||
|
||||
object NodeJS : BaseRuntime() {
|
||||
override val runtimeName: String get() = "node"
|
||||
override val bundledZipName: String get() = ""
|
||||
override val bundledZipName: String get() = "libnode.zip.so"
|
||||
override val manifestURL: String get() = ""
|
||||
|
||||
@JvmStatic
|
||||
|
||||
+56
@@ -11,6 +11,12 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdl.BuildConfig
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.core.RuntimeManager
|
||||
import com.deniscerri.ytdl.core.runtimes.Aria2c
|
||||
import com.deniscerri.ytdl.core.runtimes.BaseRuntime
|
||||
import com.deniscerri.ytdl.core.runtimes.FFmpeg
|
||||
import com.deniscerri.ytdl.core.runtimes.NodeJS
|
||||
import com.deniscerri.ytdl.core.runtimes.Python
|
||||
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
|
||||
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
|
||||
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
|
||||
@@ -110,6 +116,21 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
|
||||
true
|
||||
}
|
||||
|
||||
//packages
|
||||
findPreference<Preference>("package_python")?.apply {
|
||||
val instance = Python.getInstance()
|
||||
summary = instance.getVersion(requireContext())
|
||||
}
|
||||
findPreference<Preference>("package_ffmpeg")?.apply {
|
||||
val instance = FFmpeg.getInstance()
|
||||
summary = instance.getVersion(requireContext())
|
||||
}
|
||||
findPreference<Preference>("package_aria2c")?.apply {
|
||||
val instance = Aria2c.getInstance()
|
||||
summary = instance.getVersion(requireContext())
|
||||
}
|
||||
|
||||
handlePackage(NodeJS.getInstance(), findPreference<Preference>("package_nodejs"))
|
||||
|
||||
findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
|
||||
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) {
|
||||
@@ -185,4 +206,39 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
|
||||
}
|
||||
|
||||
|
||||
private fun handlePackage(instance: BaseRuntime, preference: Preference?) {
|
||||
//TODO REWRITE THIS LOL
|
||||
preference?.apply {
|
||||
summary = instance.getVersion(requireContext())
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
summary = getString(R.string.loading)
|
||||
val response = instance.checkForUpdates(requireContext())
|
||||
if (response == null) {
|
||||
Snackbar.make(requireView(), getString(R.string.failed_download), Snackbar.LENGTH_SHORT).show()
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val success = instance.downloadAndInstall(requireContext(), response.downloadUrl, response.version) { progress, total ->
|
||||
val downloadedMB = progress / 1048576
|
||||
val totalMB = total / 1048576
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
summary = "${getString(R.string.downloading)} $downloadedMB MB / $totalMB MB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
RuntimeManager.reInit(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
summary = instance.getVersion(requireContext())
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorAccent" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M560,800L560,720L680,720Q697,720 708.5,708.5Q720,697 720,680L720,600Q720,562 742,531Q764,500 800,487L800,473Q764,460 742,429Q720,398 720,360L720,280Q720,263 708.5,251.5Q697,240 680,240L560,240L560,160L680,160Q730,160 765,195Q800,230 800,280L800,360Q800,377 811.5,388.5Q823,400 840,400L880,400L880,560L840,560Q823,560 811.5,571.5Q800,583 800,600L800,680Q800,730 765,765Q730,800 680,800L560,800ZM280,800Q230,800 195,765Q160,730 160,680L160,600Q160,583 148.5,571.5Q137,560 120,560L80,560L80,400L120,400Q137,400 148.5,388.5Q160,377 160,360L160,280Q160,230 195,195Q230,160 280,160L400,160L400,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,398 218,429Q196,460 160,473L160,487Q196,500 218,531Q240,562 240,600L240,680Q240,697 251.5,708.5Q263,720 280,720L400,720L400,800L280,800Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -510,4 +510,6 @@
|
||||
<string name="show_quick_download_share_menu">Show option to download immediately in the share menu</string>
|
||||
<string name="quick_download_title">Don\'t fetch data</string>
|
||||
<string name="sponsorblock_hook">Hook/Greetings</string>
|
||||
<string name="packages">Packages</string>
|
||||
<string name="not_installed">Not installed</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
|
||||
<PreferenceCategory android:title="@string/source">
|
||||
@@ -75,6 +76,32 @@
|
||||
app:title="@string/changelog" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/packages">
|
||||
<Preference
|
||||
app:icon="@drawable/outline_data_object_24"
|
||||
app:key="package_python"
|
||||
app:title="Python"
|
||||
tools:summary="@string/not_installed" />
|
||||
|
||||
<Preference
|
||||
app:icon="@drawable/outline_data_object_24"
|
||||
app:key="package_ffmpeg"
|
||||
app:title="FFmpeg"
|
||||
tools:summary="@string/not_installed" />
|
||||
|
||||
<Preference
|
||||
app:icon="@drawable/outline_data_object_24"
|
||||
app:key="package_aria2c"
|
||||
app:title="Aria2c"
|
||||
tools:summary="@string/not_installed" />
|
||||
|
||||
<Preference
|
||||
app:icon="@drawable/outline_data_object_24"
|
||||
app:key="package_nodejs"
|
||||
app:title="NodeJS"
|
||||
tools:summary="@string/not_installed" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/format">
|
||||
<ListPreference
|
||||
android:defaultValue="filesize"
|
||||
|
||||
Reference in New Issue
Block a user