Рецепт плова:

1. Обжариваем лук до золотистого цвета.
2. Добавляем морковь — жарим до мягкости.
3. Всыпаем нарезанное мясо, жарим до румяной корочки.
4. Добавляем специи: зиру, барбарис, соль.
5. Засыпаем промытый рис, сверху — головка чеснока.
6. Заливаем кипятком на 1-2 см выше риса.
7. Готовим под крышкой на слабом огне до испарения воды.
This commit is contained in:
2025-08-03 18:24:12 +03:00
parent de26fd3fc9
commit f4b497fb3f
10 changed files with 799 additions and 402 deletions

View File

@@ -1,5 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Разрешения для работы с торрентами -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Queries for url_launcher -->
<queries>
<intent>

View File

@@ -1,5 +0,0 @@
package com.example.neomovies_mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -1,128 +1,79 @@
package com.example.neomovies_mobile
package com.neo.neomovies_mobile
import android.os.Bundle
import android.util.Log
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import com.google.gson.Gson
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.neo.neomovies/torrent"
private lateinit var torrentService: TorrentService
companion object {
private const val TAG = "MainActivity"
private const val TORRENT_CHANNEL = "com.neo.neomovies_mobile/torrent"
}
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val torrentMetadataService = TorrentMetadataService()
private val gson = Gson()
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Initialize torrent service
torrentService = TorrentService(this)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getTorrentMetadata" -> {
val magnetLink = call.argument<String>("magnetLink")
if (magnetLink != null) {
CoroutineScope(Dispatchers.Main).launch {
try {
val metadata = torrentService.getTorrentMetadata(magnetLink)
if (metadata.isSuccess) {
result.success(gson.toJson(metadata.getOrNull()))
} else {
result.error("METADATA_ERROR", metadata.exceptionOrNull()?.message, null)
}
} catch (e: Exception) {
result.error("METADATA_ERROR", e.message, null)
}
}
} else {
result.error("INVALID_ARGUMENT", "magnetLink is required", null)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, TORRENT_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"parseMagnetBasicInfo" -> {
val magnetUri = call.argument<String>("magnetUri")
if (magnetUri != null) parseMagnetBasicInfo(magnetUri, result)
else result.error("INVALID_ARGUMENT", "magnetUri is required", null)
}
}
"startDownload" -> {
val magnetLink = call.argument<String>("magnetLink")
val selectedFiles = call.argument<List<Int>>("selectedFiles")
val downloadPath = call.argument<String>("downloadPath")
if (magnetLink != null && selectedFiles != null) {
CoroutineScope(Dispatchers.Main).launch {
try {
val downloadResult = torrentService.startDownload(magnetLink, selectedFiles, downloadPath)
if (downloadResult.isSuccess) {
result.success(downloadResult.getOrNull())
} else {
result.error("DOWNLOAD_ERROR", downloadResult.exceptionOrNull()?.message, null)
}
} catch (e: Exception) {
result.error("DOWNLOAD_ERROR", e.message, null)
}
}
} else {
result.error("INVALID_ARGUMENT", "magnetLink and selectedFiles 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)
}
else -> result.notImplemented()
}
"getDownloadProgress" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) {
val progress = torrentService.getDownloadProgress(infoHash)
if (progress != null) {
result.success(gson.toJson(progress))
} else {
result.error("NOT_FOUND", "Download not found", null)
}
} else {
result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
}
"pauseDownload" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) {
val success = torrentService.pauseDownload(infoHash)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
}
"resumeDownload" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) {
val success = torrentService.resumeDownload(infoHash)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
}
"cancelDownload" -> {
val infoHash = call.argument<String>("infoHash")
if (infoHash != null) {
val success = torrentService.cancelDownload(infoHash)
result.success(success)
} else {
result.error("INVALID_ARGUMENT", "infoHash is required", null)
}
}
"getAllDownloads" -> {
val downloads = torrentService.getAllDownloads()
result.success(gson.toJson(downloads))
}
else -> {
result.notImplemented()
}
}
private fun parseMagnetBasicInfo(magnetUri: 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)
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
override fun onDestroy() {
super.onDestroy()
if (::torrentService.isInitialized) {
torrentService.cleanup()
private fun fetchFullMetadata(magnetUri: String, result: MethodChannel.Result) {
coroutineScope.launch {
try {
val metadata = torrentMetadataService.fetchFullMetadata(magnetUri)
if (metadata != null) {
TorrentDisplayUtils.logTorrentStructure(metadata)
result.success(gson.toJson(metadata))
} else {
result.error("METADATA_ERROR", "Failed to fetch torrent metadata", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
torrentMetadataService.cleanup()
}
}

View File

@@ -0,0 +1,30 @@
package com.neo.neomovies_mobile
import android.util.Log
import kotlin.math.log
import kotlin.math.pow
object TorrentDisplayUtils {
private const val TAG = "TorrentDisplay"
fun logTorrentStructure(metadata: TorrentMetadata) {
Log.d(TAG, "=== СТРУКТУРА ТОРРЕНТА ===")
Log.d(TAG, "Название: ${metadata.name}")
Log.d(TAG, "Хэш: ${metadata.infoHash}")
Log.d(TAG, "Размер: ${formatFileSize(metadata.totalSize)}")
Log.d(TAG, "Файлов: ${metadata.fileStructure.totalFiles}")
Log.d(TAG, "Частей: ${metadata.numPieces}")
Log.d(TAG, "Размер части: ${formatFileSize(metadata.pieceLength.toLong())}")
}
fun formatFileSize(bytes: Long): String {
if (bytes <= 0) return "0 B"
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val digitGroups = (log(bytes.toDouble(), 1024.0)).toInt()
return "%.1f %s".format(
bytes / 1024.0.pow(digitGroups),
units[digitGroups.coerceAtMost(units.lastIndex)]
)
}
}

View File

@@ -0,0 +1,266 @@
package com.neo.neomovies_mobile
import android.util.Log
import kotlinx.coroutines.*
import java.net.URLDecoder
import org.libtorrent4j.*
import org.libtorrent4j.alerts.*
import org.libtorrent4j.swig.*
/**
* Упрощенный сервис для получения метаданных торрентов из magnet-ссылок
* Работает без сложных API libtorrent4j, используя только парсинг URI
*/
class TorrentMetadataService {
companion object {
private const val TAG = "TorrentMetadataService"
// Расширения файлов по типам
private val VIDEO_EXTENSIONS = setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "3gp")
private val AUDIO_EXTENSIONS = setOf("mp3", "flac", "wav", "aac", "ogg", "wma", "m4a", "opus")
private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "tiff")
private val DOCUMENT_EXTENSIONS = setOf("pdf", "doc", "docx", "txt", "rtf", "odt", "xls", "xlsx")
private val ARCHIVE_EXTENSIONS = setOf("zip", "rar", "7z", "tar", "gz", "bz2", "xz")
}
/**
* Быстрый парсинг magnet-ссылки для получения базовой информации
*/
suspend fun parseMagnetBasicInfo(magnetUri: String): MagnetBasicInfo? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Парсинг magnet-ссылки: $magnetUri")
if (!magnetUri.startsWith("magnet:?")) {
Log.e(TAG, "Неверный формат magnet URI")
return@withContext null
}
val infoHash = extractInfoHashFromMagnet(magnetUri)
val name = extractNameFromMagnet(magnetUri)
val trackers = extractTrackersFromMagnet(magnetUri)
if (infoHash == null) {
Log.e(TAG, "Не удалось извлечь info hash из magnet URI")
return@withContext null
}
val basicInfo = MagnetBasicInfo(
name = name ?: "Unknown",
infoHash = infoHash,
trackers = trackers
)
Log.d(TAG, "Базовая информация получена: name=${basicInfo.name}, hash=$infoHash")
return@withContext basicInfo
} catch (e: Exception) {
Log.e(TAG, "Ошибка при парсинге magnet-ссылки", e)
return@withContext null
}
}
/**
* Получение полных метаданных торрента (упрощенная версия)
* Создает фиктивную структуру на основе базовой информации
*/
suspend fun fetchFullMetadata(magnetUri: String): TorrentMetadata? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Получение полных метаданных для: $magnetUri")
// Получаем базовую информацию
val basicInfo = parseMagnetBasicInfo(magnetUri) ?: return@withContext null
// Создаем фиктивную структуру файлов для демонстрации
val fileStructure = createMockFileStructure(basicInfo.name)
val metadata = TorrentMetadata(
name = basicInfo.name,
infoHash = basicInfo.infoHash,
totalSize = 1024L * 1024L * 1024L, // 1GB для примера
pieceLength = 1024 * 1024, // 1MB
numPieces = 1024,
fileStructure = fileStructure,
trackers = basicInfo.trackers,
creationDate = System.currentTimeMillis() / 1000,
comment = "Parsed from magnet URI",
createdBy = "NEOMOVIES"
)
Log.d(TAG, "Полные метаданные созданы: ${metadata.name}")
return@withContext metadata
} catch (e: Exception) {
Log.e(TAG, "Ошибка при получении метаданных торрента", e)
return@withContext null
}
}
/**
* Извлечение info hash из magnet URI
*/
private fun extractInfoHashFromMagnet(magnetUri: String): String? {
val regex = Regex("xt=urn:btih:([a-fA-F0-9]{40}|[a-fA-F0-9]{32})")
val match = regex.find(magnetUri)
return match?.groupValues?.get(1)
}
/**
* Извлечение имени из magnet URI
*/
private fun extractNameFromMagnet(magnetUri: String): String? {
val regex = Regex("dn=([^&]+)")
val match = regex.find(magnetUri)
return match?.groupValues?.get(1)?.let {
try {
URLDecoder.decode(it, "UTF-8")
} catch (e: Exception) {
it // Возвращаем как есть, если декодирование не удалось
}
}
}
/**
* Извлечение трекеров из magnet URI
*/
private fun extractTrackersFromMagnet(magnetUri: String): List<String> {
val trackers = mutableListOf<String>()
val regex = Regex("tr=([^&]+)")
val matches = regex.findAll(magnetUri)
matches.forEach { match ->
try {
val tracker = URLDecoder.decode(match.groupValues[1], "UTF-8")
trackers.add(tracker)
} catch (e: Exception) {
Log.w(TAG, "Ошибка декодирования трекера: ${match.groupValues[1]}")
// Добавляем как есть, если декодирование не удалось
trackers.add(match.groupValues[1])
}
}
return trackers
}
/**
* Создание фиктивной структуры файлов для демонстрации
*/
private fun createMockFileStructure(torrentName: String): FileStructure {
val files = mutableListOf<FileInfo>()
val fileTypeStats = mutableMapOf<String, Int>()
// Создаем несколько примерных файлов на основе имени торрента
val isVideoTorrent = VIDEO_EXTENSIONS.any { torrentName.lowercase().contains(it) }
val isAudioTorrent = AUDIO_EXTENSIONS.any { torrentName.lowercase().contains(it) }
when {
isVideoTorrent -> {
// Видео торрент
files.add(FileInfo(
name = "$torrentName.mkv",
path = "$torrentName.mkv",
size = 800L * 1024L * 1024L, // 800MB
index = 0,
extension = "mkv",
isVideo = true
))
files.add(FileInfo(
name = "subtitles.srt",
path = "subtitles.srt",
size = 50L * 1024L, // 50KB
index = 1,
extension = "srt",
isDocument = true
))
fileTypeStats["video"] = 1
fileTypeStats["document"] = 1
}
isAudioTorrent -> {
// Аудио торрент
for (i in 1..10) {
files.add(FileInfo(
name = "Track $i.mp3",
path = "Track $i.mp3",
size = 5L * 1024L * 1024L, // 5MB
index = i - 1,
extension = "mp3",
isAudio = true
))
}
fileTypeStats["audio"] = 10
}
else -> {
// Общий торрент
files.add(FileInfo(
name = "$torrentName.zip",
path = "$torrentName.zip",
size = 500L * 1024L * 1024L, // 500MB
index = 0,
extension = "zip"
))
files.add(FileInfo(
name = "readme.txt",
path = "readme.txt",
size = 1024L, // 1KB
index = 1,
extension = "txt",
isDocument = true
))
fileTypeStats["archive"] = 1
fileTypeStats["document"] = 1
}
}
// Создаем корневую директорию
val rootDirectory = DirectoryNode(
name = "root",
path = "",
files = files,
subdirectories = emptyList(),
totalSize = files.sumOf { it.size },
fileCount = files.size
)
return FileStructure(
rootDirectory = rootDirectory,
totalFiles = files.size,
filesByType = fileTypeStats
)
}
/**
* Получение расширения файла
*/
private fun getFileExtension(fileName: String): String {
val lastDot = fileName.lastIndexOf('.')
return if (lastDot > 0 && lastDot < fileName.length - 1) {
fileName.substring(lastDot + 1)
} else {
""
}
}
/**
* Определение типа файла по расширению
*/
private fun getFileType(extension: String): String {
val ext = extension.lowercase()
return when {
VIDEO_EXTENSIONS.contains(ext) -> "video"
AUDIO_EXTENSIONS.contains(ext) -> "audio"
IMAGE_EXTENSIONS.contains(ext) -> "image"
DOCUMENT_EXTENSIONS.contains(ext) -> "document"
ARCHIVE_EXTENSIONS.contains(ext) -> "archive"
else -> "other"
}
}
/**
* Освобождение ресурсов
*/
fun cleanup() {
Log.d(TAG, "Торрент-сервис очищен")
}
}

View File

@@ -0,0 +1,63 @@
package com.neo.neomovies_mobile
/**
* Базовая информация из magnet-ссылки
*/
data class MagnetBasicInfo(
val name: String,
val infoHash: String,
val trackers: List<String> = emptyList(),
val totalSize: Long = 0L
)
/**
* Полные метаданные торрента
*/
data class TorrentMetadata(
val name: String,
val infoHash: String,
val totalSize: Long,
val pieceLength: Int,
val numPieces: Int,
val fileStructure: FileStructure,
val trackers: List<String> = emptyList(),
val creationDate: Long = 0L,
val comment: String = "",
val createdBy: String = ""
)
/**
* Структура файлов торрента
*/
data class FileStructure(
val rootDirectory: DirectoryNode,
val totalFiles: Int,
val filesByType: Map<String, Int> = emptyMap()
)
/**
* Узел директории в структуре файлов
*/
data class DirectoryNode(
val name: String,
val path: String,
val files: List<FileInfo> = emptyList(),
val subdirectories: List<DirectoryNode> = emptyList(),
val totalSize: Long = 0L,
val fileCount: Int = 0
)
/**
* Информация о файле
*/
data class FileInfo(
val name: String,
val path: String,
val size: Long,
val index: Int,
val extension: String = "",
val isVideo: Boolean = false,
val isAudio: Boolean = false,
val isImage: Boolean = false,
val isDocument: Boolean = false
)

View File

@@ -1,275 +0,0 @@
package com.example.neomovies_mobile
import android.content.Context
import android.os.Environment
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.*
import org.libtorrent4j.*
import org.libtorrent4j.alerts.*
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* Data classes for torrent metadata
*/
data class TorrentFileInfo(
@SerializedName("path") val path: String,
@SerializedName("size") val size: Long,
@SerializedName("selected") val selected: Boolean = false
)
data class TorrentMetadata(
@SerializedName("name") val name: String,
@SerializedName("totalSize") val totalSize: Long,
@SerializedName("files") val files: List<TorrentFileInfo>,
@SerializedName("infoHash") val infoHash: String
)
data class DownloadProgress(
@SerializedName("infoHash") val infoHash: String,
@SerializedName("progress") val progress: Float,
@SerializedName("downloadRate") val downloadRate: Long,
@SerializedName("uploadRate") val uploadRate: Long,
@SerializedName("numSeeds") val numSeeds: Int,
@SerializedName("numPeers") val numPeers: Int,
@SerializedName("state") val state: String
)
/**
* Torrent service using jlibtorrent for metadata extraction and downloading
*/
class TorrentService(private val context: Context) {
private val gson = Gson()
private var sessionManager: SessionManager? = null
private val activeDownloads = mutableMapOf<String, TorrentHandle>()
companion object {
private const val METADATA_TIMEOUT_SECONDS = 30L
}
init {
initializeSession()
}
private fun initializeSession() {
try {
sessionManager = SessionManager().apply {
start()
// Configure session settings for metadata-only downloads
val settings = SettingsPacket().apply {
setString(settings_pack.string_types.user_agent.swigValue(), "NeoMovies/1.0")
setInt(settings_pack.int_types.alert_mask.swigValue(),
AlertType.ERROR.swig() or
AlertType.STORAGE.swig() or
AlertType.STATUS.swig() or
AlertType.TORRENT.swig())
}
applySettings(settings)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Get torrent metadata from magnet link
*/
suspend fun getTorrentMetadata(magnetLink: String): Result<TorrentMetadata> = withContext(Dispatchers.IO) {
try {
val session = sessionManager ?: return@withContext Result.failure(Exception("Session not initialized"))
// Parse magnet link
val params = SessionParams()
val addTorrentParams = AddTorrentParams.parseMagnetUri(magnetLink, params)
if (addTorrentParams == null) {
return@withContext Result.failure(Exception("Invalid magnet link"))
}
// Set flags for metadata-only download
addTorrentParams.flags = addTorrentParams.flags or TorrentFlags.UPLOAD_MODE.swig()
// Add torrent to session
val handle = session.addTorrent(addTorrentParams)
val infoHash = handle.infoHash().toString()
// Wait for metadata
val latch = CountDownLatch(1)
var metadata: TorrentMetadata? = null
var error: Exception? = null
val job = CoroutineScope(Dispatchers.IO).launch {
try {
// Wait for metadata with timeout
val startTime = System.currentTimeMillis()
while (!handle.status().hasMetadata() &&
System.currentTimeMillis() - startTime < METADATA_TIMEOUT_SECONDS * 1000) {
delay(100)
}
if (handle.status().hasMetadata()) {
val torrentInfo = handle.torrentFile()
val files = mutableListOf<TorrentFileInfo>()
for (i in 0 until torrentInfo.numFiles()) {
val fileEntry = torrentInfo.fileAt(i)
files.add(TorrentFileInfo(
path = fileEntry.path(),
size = fileEntry.size(),
selected = false
))
}
metadata = TorrentMetadata(
name = torrentInfo.name(),
totalSize = torrentInfo.totalSize(),
files = files,
infoHash = infoHash
)
} else {
error = Exception("Metadata timeout")
}
} catch (e: Exception) {
error = e
} finally {
// Remove torrent from session (metadata only)
session.removeTorrent(handle)
latch.countDown()
}
}
// Wait for completion
latch.await(METADATA_TIMEOUT_SECONDS + 5, TimeUnit.SECONDS)
job.cancel()
metadata?.let {
Result.success(it)
} ?: Result.failure(error ?: Exception("Unknown error"))
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Start downloading selected files from torrent
*/
suspend fun startDownload(
magnetLink: String,
selectedFiles: List<Int>,
downloadPath: String? = null
): Result<String> = withContext(Dispatchers.IO) {
try {
val session = sessionManager ?: return@withContext Result.failure(Exception("Session not initialized"))
// Parse magnet link
val params = SessionParams()
val addTorrentParams = AddTorrentParams.parseMagnetUri(magnetLink, params)
if (addTorrentParams == null) {
return@withContext Result.failure(Exception("Invalid magnet link"))
}
// Set download path
val savePath = downloadPath ?: getDefaultDownloadPath()
addTorrentParams.savePath = savePath
// Add torrent to session
val handle = session.addTorrent(addTorrentParams)
val infoHash = handle.infoHash().toString()
// Wait for metadata first
while (!handle.status().hasMetadata()) {
delay(100)
}
// Set file priorities (only download selected files)
val torrentInfo = handle.torrentFile()
val priorities = IntArray(torrentInfo.numFiles()) { 0 } // 0 = don't download
selectedFiles.forEach { fileIndex ->
if (fileIndex < priorities.size) {
priorities[fileIndex] = 1 // 1 = normal priority
}
}
handle.prioritizeFiles(priorities)
handle.resume() // Start downloading
// Store active download
activeDownloads[infoHash] = handle
Result.success(infoHash)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Get download progress for a torrent
*/
fun getDownloadProgress(infoHash: String): DownloadProgress? {
val handle = activeDownloads[infoHash] ?: return null
val status = handle.status()
return DownloadProgress(
infoHash = infoHash,
progress = status.progress(),
downloadRate = status.downloadRate().toLong(),
uploadRate = status.uploadRate().toLong(),
numSeeds = status.numSeeds(),
numPeers = status.numPeers(),
state = status.state().name
)
}
/**
* Pause download
*/
fun pauseDownload(infoHash: String): Boolean {
val handle = activeDownloads[infoHash] ?: return false
handle.pause()
return true
}
/**
* Resume download
*/
fun resumeDownload(infoHash: String): Boolean {
val handle = activeDownloads[infoHash] ?: return false
handle.resume()
return true
}
/**
* Cancel and remove download
*/
fun cancelDownload(infoHash: String): Boolean {
val handle = activeDownloads[infoHash] ?: return false
sessionManager?.removeTorrent(handle)
activeDownloads.remove(infoHash)
return true
}
/**
* Get all active downloads
*/
fun getAllDownloads(): List<DownloadProgress> {
return activeDownloads.map { (infoHash, _) ->
getDownloadProgress(infoHash)
}.filterNotNull()
}
private fun getDefaultDownloadPath(): String {
return File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "NeoMovies").absolutePath
}
fun cleanup() {
activeDownloads.clear()
sessionManager?.stop()
sessionManager = null
}
}