success build nodejs, todo downloadable plugins

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

3
.gitignore vendored

@ -8,4 +8,5 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
/benchmark /benchmark
*.png *.png
app/src/main/jniLibs

@ -103,6 +103,11 @@ object RuntimeManager {
initialized = true initialized = true
} }
fun reInit(context: Context) {
initialized = false
init(context)
}
private fun assertInit() { private fun assertInit() {
check(initialized) { "instance not initialized" } check(initialized) { "instance not initialized" }
} }

@ -9,6 +9,7 @@ import com.deniscerri.ytdl.core.ZipUtils
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.net.HttpURLConnection
import java.net.URL import java.net.URL
abstract class BaseRuntime { abstract class BaseRuntime {
@ -51,30 +52,92 @@ abstract class BaseRuntime {
} }
} }
fun downloadAndInstall(context: Context, zipUrl: String, versionTag: String) { fun downloadAndInstall(context: Context, zipUrl: String, versionTag: String, onProgress: (Long, Long) -> Unit) : Boolean {
val downloadDir = File(context.noBackupFilesDir, "runtimes/$runtimeName") // 1. Clean up old installation
FileUtils.deleteQuietly(downloadDir) val runtimeDir = File(context.noBackupFilesDir, "runtimes/$runtimeName")
downloadDir.mkdirs() 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 { try {
URL(zipUrl).openStream().use { input -> // 2. Download the bundle (handles GitHub 302 redirects)
tempZip.outputStream().use { output -> input.copyTo(output) } 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
}
private fun downloadWithRedirects(url: String, dest: File, onProgress: (Long, Long) -> Unit) {
var currentUrl = url
var connection: HttpURLConnection
var redirectCount = 0
ZipUtils.unzip(tempZip, downloadDir) 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
}
// Recursive function to set executable permissions on everything in bin/ val fileSize = connection.contentLength.toLong()
File(downloadDir, "bin").listFiles()?.forEach { var bytesCopied = 0L
it.setExecutable(true, false) 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)
}
} }
}
}
PreferenceManager.getDefaultSharedPreferences(context).edit { private fun applyExecutablePermissions(file: File) {
putBoolean(installedKey, true) if (file.isDirectory) {
putString(versionKey, versionTag) // Check if this folder is a 'bin' folder
if (file.name == "bin") {
file.listFiles()?.forEach { it.setExecutable(true, false) }
} }
} finally { // Recurse into subdirectories
tempZip.delete() 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? { open fun checkForUpdates(context: Context): RuntimeUpdateInfo? {
return try { return try {
val response = URL(manifestURL).readText() val response = URL(manifestURL).readText()

@ -2,7 +2,7 @@ package com.deniscerri.ytdl.core.runtimes
object NodeJS : BaseRuntime() { object NodeJS : BaseRuntime() {
override val runtimeName: String get() = "node" override val runtimeName: String get() = "node"
override val bundledZipName: String get() = "" override val bundledZipName: String get() = "libnode.zip.so"
override val manifestURL: String get() = "" override val manifestURL: String get() = ""
@JvmStatic @JvmStatic

@ -11,6 +11,12 @@ import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R 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.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment import com.deniscerri.ytdl.ui.more.settings.BaseSettingsFragment
@ -110,6 +116,21 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
true 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 { findPreference<Preference>("reset_preferences")?.setOnPreferenceClickListener {
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.reset), getString(R.string.reset_preferences_in_screen)) { 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="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="quick_download_title">Don\'t fetch data</string>
<string name="sponsorblock_hook">Hook/Greetings</string> <string name="sponsorblock_hook">Hook/Greetings</string>
<string name="packages">Packages</string>
<string name="not_installed">Not installed</string>
</resources> </resources>

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <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"> <PreferenceCategory android:title="@string/source">
@ -75,6 +76,32 @@
app:title="@string/changelog" /> app:title="@string/changelog" />
</PreferenceCategory> </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"> <PreferenceCategory android:title="@string/format">
<ListPreference <ListPreference
android:defaultValue="filesize" android:defaultValue="filesize"

Loading…
Cancel
Save