mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 23:18:49 +05:00
Compare commits
3 Commits
main
...
4b0fa8aad6
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b0fa8aad6 | |||
|
|
f8ba6c69d2 | ||
|
|
4d3413820f |
@@ -11,18 +11,24 @@ variables:
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- .gradle/
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches/modules-2
|
||||
- .pub-cache/
|
||||
- android/.gradle/
|
||||
- build/
|
||||
policy: pull-push
|
||||
|
||||
build:torrent-engine:
|
||||
stage: build
|
||||
image: mingc/android-build-box:latest
|
||||
before_script:
|
||||
- echo "sdk.dir=${ANDROID_SDK_ROOT:-/opt/android-sdk}" > android/local.properties
|
||||
# Clean up old Gradle cache to free space
|
||||
- rm -rf .gradle/caches/transforms-*
|
||||
- rm -rf .gradle/caches/*/transforms
|
||||
- find .gradle/caches -name "*.lock" -type f -delete || true
|
||||
script:
|
||||
- cd android
|
||||
- chmod +x gradlew
|
||||
- ./gradlew :torrentengine:assembleRelease --no-daemon --stacktrace
|
||||
- ./gradlew clean :torrentengine:assembleRelease --no-daemon --stacktrace
|
||||
artifacts:
|
||||
paths:
|
||||
- android/torrentengine/build/outputs/aar/*.aar
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
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 logTorrentInfo(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())}")
|
||||
Log.d(TAG, "Трекеров: ${metadata.trackers.size}")
|
||||
|
||||
if (metadata.comment.isNotEmpty()) {
|
||||
Log.d(TAG, "Комментарий: ${metadata.comment}")
|
||||
}
|
||||
if (metadata.createdBy.isNotEmpty()) {
|
||||
Log.d(TAG, "Создано: ${metadata.createdBy}")
|
||||
}
|
||||
if (metadata.creationDate > 0) {
|
||||
Log.d(TAG, "Дата создания: ${java.util.Date(metadata.creationDate * 1000)}")
|
||||
}
|
||||
|
||||
Log.d(TAG, "")
|
||||
logFileTypeStats(metadata.fileStructure)
|
||||
Log.d(TAG, "")
|
||||
logFileStructure(metadata.fileStructure)
|
||||
Log.d(TAG, "")
|
||||
logTrackerList(metadata.trackers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Выводит структуру файлов в виде дерева
|
||||
*/
|
||||
fun logFileStructure(fileStructure: FileStructure) {
|
||||
Log.d(TAG, "=== СТРУКТУРА ФАЙЛОВ ===")
|
||||
logDirectoryNode(fileStructure.rootDirectory, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Рекурсивно выводит узел директории
|
||||
*/
|
||||
private fun logDirectoryNode(node: DirectoryNode, prefix: String) {
|
||||
if (node.name.isNotEmpty()) {
|
||||
Log.d(TAG, "$prefix${node.name}/")
|
||||
}
|
||||
|
||||
val childPrefix = if (node.name.isEmpty()) prefix else "$prefix "
|
||||
|
||||
// Выводим поддиректории
|
||||
node.subdirectories.forEach { subDir ->
|
||||
Log.d(TAG, "$childPrefix├── ${subDir.name}/")
|
||||
logDirectoryNode(subDir, "$childPrefix│ ")
|
||||
}
|
||||
|
||||
// Выводим файлы
|
||||
node.files.forEachIndexed { index, file ->
|
||||
val isLast = index == node.files.size - 1 && node.subdirectories.isEmpty()
|
||||
val symbol = if (isLast) "└──" else "├──"
|
||||
val fileInfo = "${file.name} (${formatFileSize(file.size)}) [${file.extension.uppercase()}]"
|
||||
Log.d(TAG, "$childPrefix$symbol $fileInfo")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выводит статистику по типам файлов
|
||||
*/
|
||||
fun logFileTypeStats(fileStructure: FileStructure) {
|
||||
Log.d(TAG, "=== СТАТИСТИКА ПО ТИПАМ ФАЙЛОВ ===")
|
||||
if (fileStructure.filesByType.isEmpty()) {
|
||||
Log.d(TAG, "Нет статистики по типам файлов")
|
||||
return
|
||||
}
|
||||
fileStructure.filesByType.forEach { (type, count) ->
|
||||
val percentage = (count.toFloat() / fileStructure.totalFiles * 100).toInt()
|
||||
Log.d(TAG, "${type.uppercase()}: $count файлов ($percentage%)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for MainActivity – just logs structure.
|
||||
*/
|
||||
fun logTorrentStructure(metadata: TorrentMetadata) {
|
||||
logFileStructure(metadata.fileStructure)
|
||||
}
|
||||
|
||||
/**
|
||||
* Выводит список трекеров
|
||||
*/
|
||||
fun logTrackerList(trackers: List<String>) {
|
||||
if (trackers.isEmpty()) {
|
||||
Log.d(TAG, "=== ТРЕКЕРЫ === (нет трекеров)")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "=== ТРЕКЕРЫ ===")
|
||||
trackers.forEachIndexed { index, tracker ->
|
||||
Log.d(TAG, "${index + 1}. $tracker")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает текстовое представление структуры файлов
|
||||
*/
|
||||
fun getFileStructureText(fileStructure: FileStructure): String {
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("${fileStructure.rootDirectory.name}/")
|
||||
appendDirectoryNode(fileStructure.rootDirectory, "", sb)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Рекурсивно добавляет узел директории в StringBuilder
|
||||
*/
|
||||
private fun appendDirectoryNode(node: DirectoryNode, prefix: String, sb: StringBuilder) {
|
||||
val childPrefix = if (node.name.isEmpty()) prefix else "$prefix "
|
||||
|
||||
// Добавляем поддиректории
|
||||
node.subdirectories.forEach { subDir ->
|
||||
sb.appendLine("$childPrefix└── ${subDir.name}/")
|
||||
appendDirectoryNode(subDir, "$childPrefix ", sb)
|
||||
}
|
||||
|
||||
// Добавляем файлы
|
||||
node.files.forEachIndexed { index, file ->
|
||||
val isLast = index == node.files.size - 1 && node.subdirectories.isEmpty()
|
||||
val symbol = if (isLast) "└──" else "├──"
|
||||
val fileInfo = "${file.name} (${formatFileSize(file.size)})"
|
||||
sb.appendLine("$childPrefix$symbol $fileInfo")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает краткую статистику о торренте
|
||||
*/
|
||||
fun getTorrentSummary(metadata: TorrentMetadata): String {
|
||||
return buildString {
|
||||
appendLine("Название: ${metadata.name}")
|
||||
appendLine("Размер: ${formatFileSize(metadata.totalSize)}")
|
||||
appendLine("Файлов: ${metadata.fileStructure.totalFiles}")
|
||||
appendLine("Хэш: ${metadata.infoHash}")
|
||||
|
||||
if (metadata.fileStructure.filesByType.isNotEmpty()) {
|
||||
appendLine("\nТипы файлов:")
|
||||
metadata.fileStructure.filesByType.forEach { (type, count) ->
|
||||
val percentage = (count.toFloat() / metadata.fileStructure.totalFiles * 100).toInt()
|
||||
appendLine(" ${type.uppercase()}: $count ($percentage%)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует размер файла в читаемый вид
|
||||
*/
|
||||
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)]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает иконку для типа файла
|
||||
*/
|
||||
fun getFileTypeIcon(extension: String): String {
|
||||
return when {
|
||||
extension in setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "m4v", "3gp") -> "🎬"
|
||||
extension in setOf("mp3", "flac", "wav", "aac", "ogg", "wma", "m4a", "opus") -> "🎵"
|
||||
extension in setOf("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg") -> "🖼️"
|
||||
extension in setOf("pdf", "doc", "docx", "txt", "rtf", "odt") -> "📄"
|
||||
extension in setOf("zip", "rar", "7z", "tar", "gz", "bz2") -> "📦"
|
||||
else -> "📁"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Фильтрует файлы по типу
|
||||
*/
|
||||
fun filterFilesByType(files: List<FileInfo>, type: String): List<FileInfo> {
|
||||
return when (type.lowercase()) {
|
||||
"video" -> files.filter { it.isVideo }
|
||||
"audio" -> files.filter { it.isAudio }
|
||||
"image" -> files.filter { it.isImage }
|
||||
"document" -> files.filter { it.isDocument }
|
||||
"archive" -> files.filter { it.isArchive }
|
||||
else -> files
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.neo.neomovies_mobile
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.libtorrent4j.AddTorrentParams
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.libtorrent4j.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Lightweight service that exposes exactly the API used by MainActivity.
|
||||
* - parseMagnetBasicInfo: quick parsing without network.
|
||||
* - fetchFullMetadata: downloads metadata and converts to TorrentMetadata.
|
||||
* - cleanup: stops internal SessionManager.
|
||||
*/
|
||||
object TorrentMetadataService {
|
||||
|
||||
private const val TAG = "TorrentMetadataService"
|
||||
private val ioDispatcher = Dispatchers.IO
|
||||
|
||||
/** Lazy SessionManager used for metadata fetch */
|
||||
private val session: SessionManager by lazy {
|
||||
SessionManager().apply { start(SessionParams(SettingsPack())) }
|
||||
}
|
||||
|
||||
/** Parse basic info (name & hash) from magnet URI without contacting network */
|
||||
suspend fun parseMagnetBasicInfo(uri: String): MagnetBasicInfo? = withContext(ioDispatcher) {
|
||||
return@withContext try {
|
||||
MagnetBasicInfo(
|
||||
name = extractNameFromMagnet(uri),
|
||||
infoHash = extractHashFromMagnet(uri),
|
||||
trackers = emptyList<String>()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse magnet", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Download full metadata from magnet link */
|
||||
suspend fun fetchFullMetadata(uri: String): TorrentMetadata? = withContext(ioDispatcher) {
|
||||
try {
|
||||
val data = session.fetchMagnet(uri, 30, File("/tmp")) ?: return@withContext null
|
||||
val ti = TorrentInfo(data)
|
||||
return@withContext buildMetadata(ti, uri)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Metadata fetch error", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
if (session.isRunning) session.stop()
|
||||
}
|
||||
|
||||
// --- helpers
|
||||
private fun extractNameFromMagnet(uri: String): String {
|
||||
val regex = "dn=([^&]+)".toRegex()
|
||||
val match = regex.find(uri)
|
||||
return match?.groups?.get(1)?.value?.let { java.net.URLDecoder.decode(it, "UTF-8") } ?: "Unknown"
|
||||
}
|
||||
|
||||
private fun extractHashFromMagnet(uri: String): String {
|
||||
val regex = "btih:([A-Za-z0-9]{32,40})".toRegex()
|
||||
val match = regex.find(uri)
|
||||
return match?.groups?.get(1)?.value ?: ""
|
||||
}
|
||||
|
||||
private fun buildMetadata(ti: TorrentInfo, originalUri: String): TorrentMetadata {
|
||||
val fs = ti.files()
|
||||
val list = MutableList(fs.numFiles()) { idx ->
|
||||
val size = fs.fileSize(idx)
|
||||
val path = fs.filePath(idx)
|
||||
val name = File(path).name
|
||||
val ext = name.substringAfterLast('.', "").lowercase()
|
||||
FileInfo(name, path, size, idx, ext)
|
||||
}
|
||||
val root = DirectoryNode(ti.name(), "", list)
|
||||
val structure = FileStructure(root, list.size, fs.totalSize())
|
||||
return TorrentMetadata(
|
||||
name = ti.name(),
|
||||
infoHash = extractHashFromMagnet(originalUri),
|
||||
totalSize = fs.totalSize(),
|
||||
pieceLength = ti.pieceLength(),
|
||||
numPieces = ti.numPieces(),
|
||||
fileStructure = structure
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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 totalSize: Long,
|
||||
val filesByType: Map<String, Int> = emptyMap(),
|
||||
val fileTypeStats: 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,
|
||||
val isArchive: Boolean = false
|
||||
)
|
||||
Reference in New Issue
Block a user