feat: Add TorrentEngine library and new API client

- Created complete TorrentEngine library module with LibTorrent4j
  - Full torrent management (add, pause, resume, remove)
  - Magnet link metadata extraction
  - File priority management (even during download)
  - Foreground service with persistent notification
  - Room database for state persistence
  - Reactive Flow API for UI updates

- Integrated TorrentEngine with MainActivity via MethodChannel
  - addTorrent, getTorrents, pauseTorrent, resumeTorrent, removeTorrent
  - setFilePriority for dynamic file selection
  - Full JSON serialization for Flutter communication

- Created new NeoMoviesApiClient for Go-based backend
  - Email verification flow (register, verify, resendCode)
  - Google OAuth support
  - Torrent search via RedAPI
  - Multiple player support (Alloha, Lumex, Vibix)
  - Enhanced reactions system (likes/dislikes)
  - All movies/TV shows endpoints

- Updated dependencies and build configuration
  - Java 17 compatibility
  - Updated Kotlin coroutines to 1.9.0
  - Fixed build_runner version conflict
  - Added torrentengine module to settings.gradle.kts

- Added comprehensive documentation
  - TorrentEngine README with usage examples
  - DEVELOPMENT_SUMMARY with full implementation details
  - ProGuard rules for library

This is a complete rewrite of torrent functionality as a reusable library.
This commit is contained in:
factory-droid[bot]
2025-10-02 10:56:22 +00:00
parent 6a8e226a72
commit 1b28c5da45
20 changed files with 2751 additions and 743 deletions

View File

@@ -11,12 +11,12 @@ android {
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = "17"
}
defaultConfig {
@@ -44,20 +44,16 @@ flutter {
}
dependencies {
// libtorrent4j для работы с торрентами
implementation("org.libtorrent4j:libtorrent4j:2.1.0-35")
implementation("org.libtorrent4j:libtorrent4j-android-arm64:2.1.0-35")
implementation("org.libtorrent4j:libtorrent4j-android-arm:2.1.0-35")
implementation("org.libtorrent4j:libtorrent4j-android-x86:2.1.0-35")
implementation("org.libtorrent4j:libtorrent4j-android-x86_64:2.1.0-35")
// TorrentEngine library module
implementation(project(":torrentengine"))
// Kotlin Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
// Gson для JSON сериализации
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.code.gson:gson:2.11.0")
// AndroidX libraries
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
}

View File

@@ -1,8 +1,9 @@
package com.neo.neomovies_mobile
import android.os.Bundle
import android.util.Log
import com.google.gson.Gson
import com.neomovies.torrentengine.TorrentEngine
import com.neomovies.torrentengine.models.FilePriority
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@@ -17,55 +18,219 @@ class MainActivity : FlutterActivity() {
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val gson = Gson()
private lateinit var torrentEngine: TorrentEngine
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Initialize TorrentEngine
torrentEngine = TorrentEngine.getInstance(applicationContext)
torrentEngine.startStatsUpdater()
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, TORRENT_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"parseMagnetBasicInfo" -> {
"addTorrent" -> {
val magnetUri = call.argument<String>("magnetUri")
if (magnetUri != null) parseMagnetBasicInfo(magnetUri, result)
else result.error("INVALID_ARGUMENT", "magnetUri is required", null)
val savePath = call.argument<String>("savePath")
if (magnetUri != null && savePath != null) {
addTorrent(magnetUri, savePath, result)
} else {
result.error("INVALID_ARGUMENT", "magnetUri and savePath are required", null)
}
}
"fetchFullMetadata" -> {
val magnetUri = call.argument<String>("magnetUri")
if (magnetUri != null) fetchFullMetadata(magnetUri, result)
else result.error("INVALID_ARGUMENT", "magnetUri is required", null)
"getTorrents" -> getTorrents(result)
"getTorrent" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) getTorrent(infoHash, result)
else result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
"pauseTorrent" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) pauseTorrent(infoHash, result)
else result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
"resumeTorrent" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) resumeTorrent(infoHash, result)
else result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
"removeTorrent" -> {
val infoHash = call.argument<String>("infoHash")
val deleteFiles = call.argument<Boolean>("deleteFiles") ?: false
if (infoHash != null) removeTorrent(infoHash, deleteFiles, result)
else result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
"setFilePriority" -> {
val infoHash = call.argument<String>("infoHash")
val fileIndex = call.argument<Int>("fileIndex")
val priority = call.argument<Int>("priority")
if (infoHash != null && fileIndex != null && priority != null) {
setFilePriority(infoHash, fileIndex, priority, result)
} else {
result.error("INVALID_ARGUMENT", "infoHash, fileIndex, and priority are required", null)
}
}
else -> result.notImplemented()
}
}
}
private fun parseMagnetBasicInfo(magnetUri: String, result: MethodChannel.Result) {
private fun addTorrent(magnetUri: String, savePath: String, result: MethodChannel.Result) {
coroutineScope.launch {
try {
val basicInfo = TorrentMetadataService.parseMagnetBasicInfo(magnetUri)
if (basicInfo != null) {
result.success(gson.toJson(basicInfo))
} else {
result.error("PARSE_ERROR", "Failed to parse magnet URI", null)
val infoHash = withContext(Dispatchers.IO) {
torrentEngine.addTorrent(magnetUri, savePath)
}
result.success(infoHash)
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
Log.e(TAG, "Failed to add torrent", e)
result.error("ADD_TORRENT_ERROR", e.message, null)
}
}
}
private fun fetchFullMetadata(magnetUri: String, result: MethodChannel.Result) {
private fun getTorrents(result: MethodChannel.Result) {
coroutineScope.launch {
try {
val metadata = TorrentMetadataService.fetchFullMetadata(magnetUri)
if (metadata != null) {
TorrentDisplayUtils.logTorrentStructure(metadata)
result.success(gson.toJson(metadata))
val torrents = withContext(Dispatchers.IO) {
torrentEngine.getAllTorrents()
}
val torrentsJson = torrents.map { torrent ->
mapOf(
"infoHash" to torrent.infoHash,
"name" to torrent.name,
"magnetUri" to torrent.magnetUri,
"totalSize" to torrent.totalSize,
"downloadedSize" to torrent.downloadedSize,
"uploadedSize" to torrent.uploadedSize,
"downloadSpeed" to torrent.downloadSpeed,
"uploadSpeed" to torrent.uploadSpeed,
"progress" to torrent.progress,
"state" to torrent.state.name,
"numPeers" to torrent.numPeers,
"numSeeds" to torrent.numSeeds,
"savePath" to torrent.savePath,
"files" to torrent.files.map { file ->
mapOf(
"index" to file.index,
"path" to file.path,
"size" to file.size,
"downloaded" to file.downloaded,
"priority" to file.priority.value,
"progress" to file.progress
)
},
"addedDate" to torrent.addedDate,
"error" to torrent.error
)
}
result.success(gson.toJson(torrentsJson))
} catch (e: Exception) {
Log.e(TAG, "Failed to get torrents", e)
result.error("GET_TORRENTS_ERROR", e.message, null)
}
}
}
private fun getTorrent(infoHash: String, result: MethodChannel.Result) {
coroutineScope.launch {
try {
val torrent = withContext(Dispatchers.IO) {
torrentEngine.getTorrent(infoHash)
}
if (torrent != null) {
val torrentJson = mapOf(
"infoHash" to torrent.infoHash,
"name" to torrent.name,
"magnetUri" to torrent.magnetUri,
"totalSize" to torrent.totalSize,
"downloadedSize" to torrent.downloadedSize,
"uploadedSize" to torrent.uploadedSize,
"downloadSpeed" to torrent.downloadSpeed,
"uploadSpeed" to torrent.uploadSpeed,
"progress" to torrent.progress,
"state" to torrent.state.name,
"numPeers" to torrent.numPeers,
"numSeeds" to torrent.numSeeds,
"savePath" to torrent.savePath,
"files" to torrent.files.map { file ->
mapOf(
"index" to file.index,
"path" to file.path,
"size" to file.size,
"downloaded" to file.downloaded,
"priority" to file.priority.value,
"progress" to file.progress
)
},
"addedDate" to torrent.addedDate,
"error" to torrent.error
)
result.success(gson.toJson(torrentJson))
} else {
result.error("METADATA_ERROR", "Failed to fetch torrent metadata", null)
result.error("NOT_FOUND", "Torrent not found", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
Log.e(TAG, "Failed to get torrent", e)
result.error("GET_TORRENT_ERROR", e.message, null)
}
}
}
private fun pauseTorrent(infoHash: String, result: MethodChannel.Result) {
coroutineScope.launch {
try {
withContext(Dispatchers.IO) {
torrentEngine.pauseTorrent(infoHash)
}
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "Failed to pause torrent", e)
result.error("PAUSE_TORRENT_ERROR", e.message, null)
}
}
}
private fun resumeTorrent(infoHash: String, result: MethodChannel.Result) {
coroutineScope.launch {
try {
withContext(Dispatchers.IO) {
torrentEngine.resumeTorrent(infoHash)
}
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "Failed to resume torrent", e)
result.error("RESUME_TORRENT_ERROR", e.message, null)
}
}
}
private fun removeTorrent(infoHash: String, deleteFiles: Boolean, result: MethodChannel.Result) {
coroutineScope.launch {
try {
withContext(Dispatchers.IO) {
torrentEngine.removeTorrent(infoHash, deleteFiles)
}
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "Failed to remove torrent", e)
result.error("REMOVE_TORRENT_ERROR", e.message, null)
}
}
}
private fun setFilePriority(infoHash: String, fileIndex: Int, priorityValue: Int, result: MethodChannel.Result) {
coroutineScope.launch {
try {
val priority = FilePriority.fromValue(priorityValue)
withContext(Dispatchers.IO) {
torrentEngine.setFilePriority(infoHash, fileIndex, priority)
}
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "Failed to set file priority", e)
result.error("SET_PRIORITY_ERROR", e.message, null)
}
}
}
@@ -73,6 +238,6 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
TorrentMetadataService.cleanup()
torrentEngine.shutdown()
}
}