mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 22:38:50 +05:00
torrent metadata extractor finally work
This commit is contained in:
@@ -16,7 +16,6 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private val torrentMetadataService = TorrentMetadataService()
|
||||
private val gson = Gson()
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
@@ -43,7 +42,7 @@ class MainActivity : FlutterActivity() {
|
||||
private fun parseMagnetBasicInfo(magnetUri: String, result: MethodChannel.Result) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val basicInfo = torrentMetadataService.parseMagnetBasicInfo(magnetUri)
|
||||
val basicInfo = TorrentMetadataService.parseMagnetBasicInfo(magnetUri)
|
||||
if (basicInfo != null) {
|
||||
result.success(gson.toJson(basicInfo))
|
||||
} else {
|
||||
@@ -58,7 +57,7 @@ class MainActivity : FlutterActivity() {
|
||||
private fun fetchFullMetadata(magnetUri: String, result: MethodChannel.Result) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val metadata = torrentMetadataService.fetchFullMetadata(magnetUri)
|
||||
val metadata = TorrentMetadataService.fetchFullMetadata(magnetUri)
|
||||
if (metadata != null) {
|
||||
TorrentDisplayUtils.logTorrentStructure(metadata)
|
||||
result.success(gson.toJson(metadata))
|
||||
@@ -74,6 +73,6 @@ class MainActivity : FlutterActivity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
torrentMetadataService.cleanup()
|
||||
TorrentMetadataService.cleanup()
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,161 @@ object TorrentDisplayUtils {
|
||||
|
||||
private const val TAG = "TorrentDisplay"
|
||||
|
||||
fun logTorrentStructure(metadata: TorrentMetadata) {
|
||||
Log.d(TAG, "=== СТРУКТУРА ТОРРЕНТА ===")
|
||||
/**
|
||||
* Выводит полную информацию о торренте в лог
|
||||
*/
|
||||
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")
|
||||
@@ -27,4 +172,32 @@ object TorrentDisplayUtils {
|
||||
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,266 +1,90 @@
|
||||
package com.neo.neomovies_mobile
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.URLDecoder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.libtorrent4j.AddTorrentParams
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.libtorrent4j.*
|
||||
import org.libtorrent4j.alerts.*
|
||||
import org.libtorrent4j.swig.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Упрощенный сервис для получения метаданных торрентов из magnet-ссылок
|
||||
* Работает без сложных API libtorrent4j, используя только парсинг URI
|
||||
* 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.
|
||||
*/
|
||||
class TorrentMetadataService {
|
||||
object TorrentMetadataService {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TorrentMetadataService"
|
||||
private const val TAG = "TorrentMetadataService"
|
||||
private val ioDispatcher = Dispatchers.IO
|
||||
|
||||
// Расширения файлов по типам
|
||||
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")
|
||||
/** Lazy SessionManager used for metadata fetch */
|
||||
private val session: SessionManager by lazy {
|
||||
SessionManager().apply { start(SessionParams(SettingsPack())) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Быстрый парсинг 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
|
||||
/** 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>()
|
||||
)
|
||||
|
||||
Log.d(TAG, "Базовая информация получена: name=${basicInfo.name}, hash=$infoHash")
|
||||
return@withContext basicInfo
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Ошибка при парсинге magnet-ссылки", e)
|
||||
return@withContext null
|
||||
Log.e(TAG, "Failed to parse magnet", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение полных метаданных торрента (упрощенная версия)
|
||||
* Создает фиктивную структуру на основе базовой информации
|
||||
*/
|
||||
suspend fun fetchFullMetadata(magnetUri: String): TorrentMetadata? = withContext(Dispatchers.IO) {
|
||||
/** Download full metadata from magnet link */
|
||||
suspend fun fetchFullMetadata(uri: String): TorrentMetadata? = withContext(ioDispatcher) {
|
||||
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
|
||||
|
||||
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, "Ошибка при получении метаданных торрента", e)
|
||||
return@withContext null
|
||||
Log.e(TAG, "Metadata fetch error", e)
|
||||
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, "Торрент-сервис очищен")
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ data class TorrentMetadata(
|
||||
data class FileStructure(
|
||||
val rootDirectory: DirectoryNode,
|
||||
val totalFiles: Int,
|
||||
val filesByType: Map<String, Int> = emptyMap()
|
||||
val totalSize: Long,
|
||||
val filesByType: Map<String, Int> = emptyMap(),
|
||||
val fileTypeStats: Map<String, Int> = emptyMap()
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -59,5 +61,6 @@ data class FileInfo(
|
||||
val isVideo: Boolean = false,
|
||||
val isAudio: Boolean = false,
|
||||
val isImage: Boolean = false,
|
||||
val isDocument: Boolean = false
|
||||
val isDocument: Boolean = false,
|
||||
val isArchive: Boolean = false
|
||||
)
|
||||
Reference in New Issue
Block a user