Merge branch 'feature/torrent-engine-integration' into 'main'

fix(build): resolve Gradle and manifest issues for TorrentEngine

See merge request foxixus/neomovies_mobile!2
This commit is contained in:
2025-10-02 13:51:36 +00:00
5 changed files with 66 additions and 48 deletions

View File

@@ -16,5 +16,5 @@ kotlin.incremental=true
kotlin.incremental.usePreciseJavaTracking=true kotlin.incremental.usePreciseJavaTracking=true
# Build optimization # Build optimization
android.enableBuildCache=true # android.enableBuildCache=true # Deprecated in AGP 7.0+, use org.gradle.caching instead
org.gradle.vfs.watch=false org.gradle.vfs.watch=false

View File

@@ -28,7 +28,7 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false id("com.android.application") version "8.7.3" apply false
id("com.android.library") version "8.7.3" apply false id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.android") version "1.9.24" apply false
} }
include(":app") include(":app")

View File

@@ -62,6 +62,7 @@ dependencies {
implementation("com.google.code.gson:gson:2.11.0") implementation("com.google.code.gson:gson:2.11.0")
// LibTorrent4j - Java bindings for libtorrent // LibTorrent4j - Java bindings for libtorrent
// Using main package which includes native libraries
implementation("org.libtorrent4j:libtorrent4j:2.1.0-28") implementation("org.libtorrent4j:libtorrent4j:2.1.0-28")
implementation("org.libtorrent4j:libtorrent4j-android-arm64:2.1.0-28") implementation("org.libtorrent4j:libtorrent4j-android-arm64:2.1.0-28")
implementation("org.libtorrent4j:libtorrent4j-android-arm:2.1.0-28") implementation("org.libtorrent4j:libtorrent4j-android-arm:2.1.0-28")

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions for torrent engine --> <!-- Permissions for torrent engine -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.libtorrent4j.* import org.libtorrent4j.*
import org.libtorrent4j.alerts.* import org.libtorrent4j.alerts.*
import org.libtorrent4j.TorrentInfo as LibTorrentInfo
import java.io.File import java.io.File
/** /**
@@ -41,13 +42,17 @@ class TorrentEngine private constructor(private val context: Context) {
private val torrentHandles = mutableMapOf<String, TorrentHandle>() private val torrentHandles = mutableMapOf<String, TorrentHandle>()
// Settings // Settings
private val settings = SettingsPack().apply { private val settingsPack = SettingsPack().apply {
setInteger(SettingsPack.Key.ALERT_MASK.value(), Alert.Category.ALL.swig()) // Enable DHT for magnet links
setBoolean(SettingsPack.Key.ENABLE_DHT.value(), true) setEnableDht(true)
setBoolean(SettingsPack.Key.ENABLE_LSD.value(), true) // Enable Local Service Discovery
setString(SettingsPack.Key.USER_AGENT.value(), "NeoMovies/1.0 libtorrent4j/2.1.0") setEnableLsd(true)
// User agent
setString(org.libtorrent4j.swig.settings_pack.string_types.user_agent.swigValue(), "NeoMovies/1.0 libtorrent4j/2.1.0")
} }
private val sessionParams = SessionParams(settingsPack)
init { init {
startSession() startSession()
restoreTorrents() restoreTorrents()
@@ -60,7 +65,7 @@ class TorrentEngine private constructor(private val context: Context) {
private fun startSession() { private fun startSession() {
try { try {
session = SessionManager() session = SessionManager()
session?.start(settings) session?.start(sessionParams)
isSessionStarted = true isSessionStarted = true
Log.d(TAG, "LibTorrent session started") Log.d(TAG, "LibTorrent session started")
} catch (e: Exception) { } catch (e: Exception) {
@@ -93,21 +98,21 @@ class TorrentEngine private constructor(private val context: Context) {
* Start alert listener for torrent events * Start alert listener for torrent events
*/ */
private fun startAlertListener() { private fun startAlertListener() {
scope.launch { session?.addListener(object : AlertListener {
while (isActive && isSessionStarted) { override fun types(): IntArray {
try { return intArrayOf(
session?.let { sess -> AlertType.METADATA_RECEIVED.swig(),
val alerts = sess.popAlerts() AlertType.TORRENT_FINISHED.swig(),
for (alert in alerts) { AlertType.TORRENT_ERROR.swig(),
handleAlert(alert) AlertType.STATE_CHANGED.swig(),
} AlertType.TORRENT_CHECKED.swig()
} )
delay(1000) // Check every second
} catch (e: Exception) {
Log.e(TAG, "Error in alert listener", e)
}
} }
}
override fun alert(alert: Alert<*>) {
handleAlert(alert)
}
})
} }
/** /**
@@ -191,7 +196,8 @@ class TorrentEngine private constructor(private val context: Context) {
scope.launch { scope.launch {
val handle = alert.handle() val handle = alert.handle()
val infoHash = handle.infoHash().toHex() val infoHash = handle.infoHash().toHex()
val error = alert.error().message() // message is a property in Kotlin
val error = alert.error().message
Log.e(TAG, "Torrent error: $infoHash - $error") Log.e(TAG, "Torrent error: $infoHash - $error")
torrentDao.setTorrentError(infoHash, error) torrentDao.setTorrentError(infoHash, error)
@@ -205,7 +211,8 @@ class TorrentEngine private constructor(private val context: Context) {
scope.launch { scope.launch {
val handle = alert.handle() val handle = alert.handle()
val infoHash = handle.infoHash().toHex() val infoHash = handle.infoHash().toHex()
val state = when (alert.state()) { val status = handle.status()
val state = when (status.state()) {
TorrentStatus.State.CHECKING_FILES -> TorrentState.CHECKING TorrentStatus.State.CHECKING_FILES -> TorrentState.CHECKING
TorrentStatus.State.DOWNLOADING_METADATA -> TorrentState.METADATA_DOWNLOADING TorrentStatus.State.DOWNLOADING_METADATA -> TorrentState.METADATA_DOWNLOADING
TorrentStatus.State.DOWNLOADING -> TorrentState.DOWNLOADING TorrentStatus.State.DOWNLOADING -> TorrentState.DOWNLOADING
@@ -251,15 +258,11 @@ class TorrentEngine private constructor(private val context: Context) {
): String { ): String {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
// Parse magnet URI // Parse magnet URI using new API
val error = ErrorCode() val params = AddTorrentParams.parseMagnetUri(magnetUri)
val params = SessionHandle.parseMagnetUri(magnetUri, error)
if (error.value() != 0) { // Get info hash from parsed params - best is a property
throw Exception("Invalid magnet URI: ${error.message()}") val infoHash = params.infoHashes.best.toHex()
}
val infoHash = params.infoHash().toHex()
// Check if already exists // Check if already exists
val existing = existingTorrent ?: torrentDao.getTorrent(infoHash) val existing = existingTorrent ?: torrentDao.getTorrent(infoHash)
@@ -268,22 +271,16 @@ class TorrentEngine private constructor(private val context: Context) {
return@withContext infoHash return@withContext infoHash
} }
// Set save path // Set save path and apply to params
val saveDir = File(savePath) val saveDir = File(savePath)
if (!saveDir.exists()) { if (!saveDir.exists()) {
saveDir.mkdirs() saveDir.mkdirs()
} }
params.savePath(saveDir.absolutePath) params.swig().setSave_path(saveDir.absolutePath)
// Add to session // Add to session using async API
val handle = session?.swig()?.addTorrent(params, error) // Handle will be received asynchronously via ADD_TORRENT alert
?: throw Exception("Session not initialized") session?.swig()?.async_add_torrent(params.swig()) ?: throw Exception("Session not initialized")
if (error.value() != 0) {
throw Exception("Failed to add torrent: ${error.message()}")
}
torrentHandles[infoHash] = TorrentHandle(handle)
// Save to database // Save to database
val torrentInfo = TorrentInfo( val torrentInfo = TorrentInfo(
@@ -334,9 +331,11 @@ class TorrentEngine private constructor(private val context: Context) {
Log.d(TAG, "Torrent paused: $infoHash") Log.d(TAG, "Torrent paused: $infoHash")
// Stop service if no active torrents // Stop service if no active torrents
if (torrentDao.getActiveTorrents().isEmpty()) { val activeTorrents = torrentDao.getActiveTorrents()
if (activeTorrents.isEmpty()) {
stopService() stopService()
} }
Unit // Explicitly return Unit
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to pause torrent", e) Log.e(TAG, "Failed to pause torrent", e)
} }
@@ -372,9 +371,11 @@ class TorrentEngine private constructor(private val context: Context) {
Log.d(TAG, "Torrent removed: $infoHash") Log.d(TAG, "Torrent removed: $infoHash")
// Stop service if no active torrents // Stop service if no active torrents
if (torrentDao.getActiveTorrents().isEmpty()) { val activeTorrents = torrentDao.getActiveTorrents()
if (activeTorrents.isEmpty()) {
stopService() stopService()
} }
Unit // Explicitly return Unit
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to remove torrent", e) Log.e(TAG, "Failed to remove torrent", e)
} }
@@ -393,7 +394,15 @@ class TorrentEngine private constructor(private val context: Context) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val handle = torrentHandles[infoHash] ?: return@withContext val handle = torrentHandles[infoHash] ?: return@withContext
handle.filePriority(fileIndex, Priority.getValue(priority.value)) // Convert FilePriority to LibTorrent Priority
val libPriority = when (priority) {
FilePriority.DONT_DOWNLOAD -> Priority.IGNORE
FilePriority.LOW -> Priority.LOW
FilePriority.NORMAL -> Priority.DEFAULT
FilePriority.HIGH -> Priority.TOP_PRIORITY
else -> Priority.DEFAULT // Default
}
handle.filePriority(fileIndex, libPriority)
// Update database // Update database
val torrent = torrentDao.getTorrent(infoHash) ?: return@withContext val torrent = torrentDao.getTorrent(infoHash) ?: return@withContext
@@ -418,7 +427,14 @@ class TorrentEngine private constructor(private val context: Context) {
val handle = torrentHandles[infoHash] ?: return@withContext val handle = torrentHandles[infoHash] ?: return@withContext
priorities.forEach { (fileIndex, priority) -> priorities.forEach { (fileIndex, priority) ->
handle.filePriority(fileIndex, Priority.getValue(priority.value)) val libPriority = when (priority) {
FilePriority.DONT_DOWNLOAD -> Priority.IGNORE
FilePriority.LOW -> Priority.LOW
FilePriority.NORMAL -> Priority.DEFAULT
FilePriority.HIGH -> Priority.TOP_PRIORITY
else -> Priority.DEFAULT // Default
}
handle.filePriority(fileIndex, libPriority)
} }
// Update database // Update database