mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-27 22:38:50 +05:00
better
This commit is contained in:
222
.gitlab-ci.yml
222
.gitlab-ci.yml
@@ -2,14 +2,50 @@ stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
build:apk:
|
||||
variables:
|
||||
FLUTTER_VERSION: "stable"
|
||||
|
||||
build:apk:arm64:
|
||||
stage: build
|
||||
image: cirrusci/flutter:latest
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter build apk --release
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-arm64 --split-per-abi
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/*.apk
|
||||
- build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
expire_in: 30 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||
when: on_success
|
||||
|
||||
build:apk:arm:
|
||||
stage: build
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-arm --split-per-abi
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
expire_in: 30 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
- if: $CI_COMMIT_BRANCH =~ /^dev|main/
|
||||
when: on_success
|
||||
|
||||
build:apk:x64:
|
||||
stage: build
|
||||
image: ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION}
|
||||
script:
|
||||
- flutter pub get
|
||||
- flutter build apk --release --target-platform android-x64 --split-per-abi
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
expire_in: 30 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
@@ -21,52 +57,162 @@ deploy:release:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
needs:
|
||||
- build:apk
|
||||
- build:apk:arm64
|
||||
- build:apk:arm
|
||||
- build:apk:x64
|
||||
before_script:
|
||||
- apk add --no-cache curl jq
|
||||
- apk add --no-cache curl jq coreutils
|
||||
script:
|
||||
- |
|
||||
VERSION="${CI_COMMIT_TAG:-v0.0.${CI_PIPELINE_ID}}"
|
||||
APK_FILE=$(ls build/app/outputs/flutter-apk/*.apk | head -n1)
|
||||
if [ -z "$APK_FILE" ]; then
|
||||
echo "No APK found!"
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
VERSION="$CI_COMMIT_TAG"
|
||||
else
|
||||
VERSION="v0.0.${CI_PIPELINE_ID}"
|
||||
fi
|
||||
|
||||
echo "Creating GitLab Release: $VERSION"
|
||||
echo "Commit: ${CI_COMMIT_SHORT_SHA}"
|
||||
echo "Branch: ${CI_COMMIT_BRANCH}"
|
||||
|
||||
APK_ARM64="build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
|
||||
APK_ARM32="build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk"
|
||||
APK_X86="build/app/outputs/flutter-apk/app-x86_64-release.apk"
|
||||
|
||||
RELEASE_DESCRIPTION="## NeoMovies Mobile ${VERSION}
|
||||
|
||||
**Build Info:**
|
||||
- Commit: \`${CI_COMMIT_SHORT_SHA}\`
|
||||
- Branch: \`${CI_COMMIT_BRANCH}\`
|
||||
- Pipeline: [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL})
|
||||
|
||||
**Downloads:**"
|
||||
|
||||
FILE_COUNT=0
|
||||
|
||||
if [ -f "$APK_ARM64" ]; then
|
||||
FILE_COUNT=$((FILE_COUNT+1))
|
||||
SIZE_ARM64=$(du -h "$APK_ARM64" | cut -f1)
|
||||
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM64 (arm64-v8a): \`app-arm64-v8a-release.apk\` (${SIZE_ARM64}) - Recommended for modern devices"
|
||||
fi
|
||||
|
||||
if [ -f "$APK_ARM32" ]; then
|
||||
FILE_COUNT=$((FILE_COUNT+1))
|
||||
SIZE_ARM32=$(du -h "$APK_ARM32" | cut -f1)
|
||||
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM32 (armeabi-v7a): \`app-armeabi-v7a-release.apk\` (${SIZE_ARM32}) - For older devices"
|
||||
fi
|
||||
|
||||
if [ -f "$APK_X86" ]; then
|
||||
FILE_COUNT=$((FILE_COUNT+1))
|
||||
SIZE_X86=$(du -h "$APK_X86" | cut -f1)
|
||||
RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- x86_64: \`app-x86_64-release.apk\` (${SIZE_X86}) - For emulators"
|
||||
fi
|
||||
|
||||
if [ $FILE_COUNT -eq 0 ]; then
|
||||
echo "No release artifacts found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DESCRIPTION="NeoMovies Mobile ${VERSION}
|
||||
|
||||
Commit: ${CI_COMMIT_SHORT_SHA}
|
||||
Branch: ${CI_COMMIT_BRANCH}
|
||||
Pipeline: [#${CI_PIPELINE_ID}](${CI_PIPELINE_URL})
|
||||
|
||||
APK: \`$(basename $APK_FILE)\`"
|
||||
|
||||
RELEASE_PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"name": "NeoMovies ${VERSION}",
|
||||
"tag_name": "${VERSION}",
|
||||
"description": "${DESCRIPTION}",
|
||||
"ref": "${CI_COMMIT_SHA}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
curl -s --fail -X POST "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases" \
|
||||
|
||||
echo "Found $FILE_COUNT artifact(s) to release"
|
||||
|
||||
RELEASE_DATA=$(jq -n \
|
||||
--arg name "NeoMovies ${VERSION}" \
|
||||
--arg tag "${VERSION}" \
|
||||
--arg desc "$RELEASE_DESCRIPTION" \
|
||||
--arg ref "${CI_COMMIT_SHA}" \
|
||||
'{name: $name, tag_name: $tag, description: $desc, ref: $ref}')
|
||||
|
||||
echo "Creating release via GitLab API..."
|
||||
|
||||
curl --fail-with-body -s -X POST \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases" \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$RELEASE_PAYLOAD" || \
|
||||
curl -s -X PUT "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}" \
|
||||
--data "$RELEASE_DATA" || \
|
||||
curl -s -X PUT \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}" \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$RELEASE_PAYLOAD"
|
||||
|
||||
echo "Release created/updated: ${CI_PROJECT_URL}/-/releases/${VERSION}"
|
||||
echo "APK artifact: ${CI_JOB_URL}/artifacts/browse"
|
||||
|
||||
--data "$RELEASE_DATA"
|
||||
|
||||
echo ""
|
||||
echo "Uploading APK files to Package Registry..."
|
||||
|
||||
if [ -f "$APK_ARM64" ]; then
|
||||
echo "Uploading app-arm64-v8a-release.apk..."
|
||||
curl --fail -s --request PUT \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--upload-file "$APK_ARM64" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-arm64-v8a-release.apk"
|
||||
|
||||
LINK_DATA=$(jq -n \
|
||||
--arg name "app-arm64-v8a-release.apk" \
|
||||
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-arm64-v8a-release.apk" \
|
||||
--arg type "package" \
|
||||
'{name: $name, url: $url, link_type: $type}')
|
||||
|
||||
curl -s --request POST \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$LINK_DATA" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||
|
||||
echo "ARM64 APK uploaded"
|
||||
fi
|
||||
|
||||
if [ -f "$APK_ARM32" ]; then
|
||||
echo "Uploading app-armeabi-v7a-release.apk..."
|
||||
curl --fail -s --request PUT \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--upload-file "$APK_ARM32" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-armeabi-v7a-release.apk"
|
||||
|
||||
LINK_DATA=$(jq -n \
|
||||
--arg name "app-armeabi-v7a-release.apk" \
|
||||
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-armeabi-v7a-release.apk" \
|
||||
--arg type "package" \
|
||||
'{name: $name, url: $url, link_type: $type}')
|
||||
|
||||
curl -s --request POST \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$LINK_DATA" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||
|
||||
echo "ARM32 APK uploaded"
|
||||
fi
|
||||
|
||||
if [ -f "$APK_X86" ]; then
|
||||
echo "Uploading app-x86_64-release.apk..."
|
||||
curl --fail -s --request PUT \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--upload-file "$APK_X86" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-x86_64-release.apk"
|
||||
|
||||
LINK_DATA=$(jq -n \
|
||||
--arg name "app-x86_64-release.apk" \
|
||||
--arg url "${CI_PROJECT_URL}/-/package_files/${CI_PROJECT_ID}/packages/generic/neomovies/${VERSION}/app-x86_64-release.apk" \
|
||||
--arg type "package" \
|
||||
'{name: $name, url: $url, link_type: $type}')
|
||||
|
||||
curl -s --request POST \
|
||||
--header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$LINK_DATA" \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${VERSION}/assets/links"
|
||||
|
||||
echo "x86_64 APK uploaded"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "Release created successfully!"
|
||||
echo "View release: ${CI_PROJECT_URL}/-/releases/${VERSION}"
|
||||
echo "Pipeline artifacts: ${CI_JOB_URL}/artifacts/browse"
|
||||
echo "================================================"
|
||||
artifacts:
|
||||
paths:
|
||||
- build/app/outputs/flutter-apk/*.apk
|
||||
expire_in: 30 days
|
||||
expire_in: 90 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: always
|
||||
|
||||
@@ -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