diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1634f9d..33bf8e2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,202 +1,201 @@ -# GitLab CI/CD Configuration for NeoMovies Mobile -# Автоматическая сборка APK и TorrentEngine модуля - stages: - build - test - deploy variables: - # Flutter версия - FLUTTER_VERSION: "3.35.5" - # Flutter путь для CI - FLUTTER_ROOT: "/opt/flutter" - # Android SDK (стандартный путь в mingc/android-build-box) - ANDROID_SDK_ROOT: "/opt/android-sdk" - ANDROID_HOME: "/opt/android-sdk" - # Gradle настройки для CI (меньше RAM) - GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs='-Xmx1536m -XX:MaxMetaspaceSize=512m' -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" - # Кэш + GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs='-Xmx2048m' -Dorg.gradle.parallel=true" GRADLE_USER_HOME: "${CI_PROJECT_DIR}/.gradle" PUB_CACHE: "${CI_PROJECT_DIR}/.pub-cache" -# Кэширование для ускорения сборки cache: key: ${CI_COMMIT_REF_SLUG} paths: - .gradle/ - .pub-cache/ - android/.gradle/ - - android/build/ - build/ -# ============================================ -# Сборка только TorrentEngine модуля -# ============================================ build:torrent-engine: stage: build image: mingc/android-build-box:latest - tags: - - saas-linux-medium-amd64 # GitLab Instance Runner (4GB RAM, 2 cores) - before_script: - - echo "Detecting Android SDK location..." - - export ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/opt/android-sdk}} - - echo "Android SDK: ${ANDROID_SDK_ROOT}" - - echo "Creating local.properties for Flutter..." - - echo "flutter.sdk=${FLUTTER_ROOT}" > android/local.properties - - echo "sdk.dir=${ANDROID_SDK_ROOT}" >> android/local.properties - - cat android/local.properties script: - - echo "Building TorrentEngine library module..." - cd android - # Собираем только модуль torrentengine - - ./gradlew :torrentengine:assembleRelease --no-daemon --parallel --build-cache - - ls -lah torrentengine/build/outputs/aar/ + - chmod +x gradlew + - ./gradlew :torrentengine:assembleRelease --no-daemon --stacktrace artifacts: - name: "torrentengine-${CI_COMMIT_SHORT_SHA}" paths: - android/torrentengine/build/outputs/aar/*.aar - expire_in: 1 week - only: - - dev - - feature/torrent-engine-integration - - merge_requests - when: on_success + expire_in: 30 days + rules: + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH == "main" + - if: $CI_COMMIT_BRANCH =~ /^feature\// + - if: $CI_COMMIT_TAG + - if: $CI_PIPELINE_SOURCE == "merge_request_event" -# ============================================ -# Сборка Debug APK -# ============================================ build:apk-debug: stage: build - image: mingc/android-build-box:latest - tags: - - docker - before_script: - - echo "Installing Flutter ${FLUTTER_VERSION}..." - - git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git /opt/flutter - - export PATH="/opt/flutter/bin:$PATH" - - flutter --version - - flutter doctor -v - - flutter pub get + image: ghcr.io/cirruslabs/flutter:stable script: - - echo "Building Debug APK..." - - flutter build apk --debug --target-platform android-arm64 - - ls -lah build/app/outputs/flutter-apk/ + - flutter pub get + - flutter build apk --debug artifacts: - name: "neomovies-debug-${CI_COMMIT_SHORT_SHA}" paths: - build/app/outputs/flutter-apk/app-debug.apk expire_in: 1 week - only: - - dev - - feature/torrent-engine-integration - - merge_requests - when: on_success + rules: + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH =~ /^feature\// + - if: $CI_PIPELINE_SOURCE == "merge_request_event" allow_failure: true -# ============================================ -# Сборка Release APK (только для dev) -# ============================================ build:apk-release: stage: build - image: mingc/android-build-box:latest - tags: - - docker - before_script: - - echo "Installing Flutter ${FLUTTER_VERSION}..." - - git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git /opt/flutter - - export PATH="/opt/flutter/bin:$PATH" - - flutter --version - - flutter doctor -v - - flutter pub get + image: ghcr.io/cirruslabs/flutter:stable script: - - echo "Building Release APK..." - # Сборка с split-per-abi для уменьшения размера - - flutter build apk --release --split-per-abi --target-platform android-arm64 - - ls -lah build/app/outputs/flutter-apk/ + - flutter pub get + - flutter build apk --release --split-per-abi artifacts: - name: "neomovies-release-${CI_COMMIT_SHORT_SHA}" paths: - build/app/outputs/flutter-apk/app-arm64-v8a-release.apk + - build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk + - build/app/outputs/flutter-apk/app-x86_64-release.apk expire_in: 30 days - only: - - dev - when: on_success + rules: + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH == "main" + - if: $CI_COMMIT_BRANCH =~ /^feature\// + - if: $CI_COMMIT_TAG allow_failure: true -# ============================================ -# Анализ кода Flutter -# ============================================ test:flutter-analyze: stage: test - image: mingc/android-build-box:latest - tags: - - docker - before_script: - - git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git /opt/flutter - - export PATH="/opt/flutter/bin:$PATH" - - flutter pub get + image: ghcr.io/cirruslabs/flutter:stable script: - - echo "Running Flutter analyze..." - - flutter analyze --no-fatal-infos || true - only: - - dev - - merge_requests + - flutter pub get + - flutter analyze + rules: + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_PIPELINE_SOURCE == "merge_request_event" allow_failure: true -# ============================================ -# Kotlin/Android lint -# ============================================ test:android-lint: stage: test image: mingc/android-build-box:latest - tags: - - docker - before_script: - - echo "Creating local.properties for Flutter..." - - echo "flutter.sdk=${FLUTTER_ROOT}" > android/local.properties - - echo "sdk.dir=${ANDROID_SDK_ROOT}" >> android/local.properties script: - - echo "Running Android Lint..." - cd android - - ./gradlew lint --no-daemon || true + - chmod +x gradlew + - ./gradlew lint --no-daemon artifacts: - name: "lint-reports-${CI_COMMIT_SHORT_SHA}" paths: - - android/app/build/reports/lint-results*.html - - android/torrentengine/build/reports/lint-results*.html + - android/app/build/reports/lint-*.html + - android/torrentengine/build/reports/lint-*.html expire_in: 1 week - only: - - dev - - merge_requests + when: always + rules: + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_PIPELINE_SOURCE == "merge_request_event" allow_failure: true -# ============================================ -# Deploy к релизам (опционально) -# ============================================ deploy:release: stage: deploy image: alpine:latest - tags: - - docker + needs: + - build:apk-release + - build:torrent-engine before_script: - apk add --no-cache curl jq script: - - echo "Creating GitLab Release..." - | - if [ -f "build/app/outputs/flutter-apk/app-arm64-v8a-release.apk" ]; then - echo "Release APK found" - # Здесь можно добавить публикацию в GitLab Releases или другой deployment + # Определяем версию релиза + if [ -n "$CI_COMMIT_TAG" ]; then + VERSION="$CI_COMMIT_TAG" + else + # Автоматическая версия из коммита + VERSION="v0.0.${CI_PIPELINE_ID}" fi - only: - - tags - when: manual - -# ============================================ -# Уведомление об успешной сборке -# ============================================ -.notify_success: - after_script: - - echo "✅ Build completed successfully!" - - echo "📦 Artifacts are available in the pipeline artifacts" - - echo "🔗 Download URL: ${CI_JOB_URL}/artifacts/download" + + echo "📦 Creating GitLab Release: $VERSION" + echo "📝 Commit: ${CI_COMMIT_SHORT_SHA}" + echo "🔗 Branch: ${CI_COMMIT_BRANCH}" + + # Проверяем наличие APK файлов + 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" + AAR_TORRENT="android/torrentengine/build/outputs/aar/torrentengine-release.aar" + + # Создаем описание релиза + 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 + [ -f "$APK_ARM64" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM64 APK: \`app-arm64-v8a-release.apk\`" + [ -f "$APK_ARM32" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- ARM32 APK: \`app-armeabi-v7a-release.apk\`" + [ -f "$APK_X86" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- x86_64 APK: \`app-x86_64-release.apk\`" + [ -f "$AAR_TORRENT" ] && FILE_COUNT=$((FILE_COUNT+1)) && RELEASE_DESCRIPTION="${RELEASE_DESCRIPTION}\n- TorrentEngine Library: \`torrentengine-release.aar\`" + + if [ $FILE_COUNT -eq 0 ]; then + echo "❌ No release artifacts found!" + exit 1 + fi + + echo "✅ Found $FILE_COUNT artifact(s) to release" + + # Создаем релиз через GitLab API + RELEASE_PAYLOAD=$(cat < - + diff --git a/android/torrentengine/src/main/java/com/neomovies/torrentengine/TorrentEngine.kt b/android/torrentengine/src/main/java/com/neomovies/torrentengine/TorrentEngine.kt index 01c6290..166fed9 100644 --- a/android/torrentengine/src/main/java/com/neomovies/torrentengine/TorrentEngine.kt +++ b/android/torrentengine/src/main/java/com/neomovies/torrentengine/TorrentEngine.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.libtorrent4j.* import org.libtorrent4j.alerts.* +import org.libtorrent4j.TorrentInfo as LibTorrentInfo import java.io.File /** @@ -41,12 +42,16 @@ class TorrentEngine private constructor(private val context: Context) { private val torrentHandles = mutableMapOf() // Settings - private val settings = SettingsPack().apply { - setInteger(SettingsPack.Key.ALERT_MASK.value(), Alert.Category.ALL.swig()) - setBoolean(SettingsPack.Key.ENABLE_DHT.value(), true) - setBoolean(SettingsPack.Key.ENABLE_LSD.value(), true) - setString(SettingsPack.Key.USER_AGENT.value(), "NeoMovies/1.0 libtorrent4j/2.1.0") + private val settingsPack = SettingsPack().apply { + // Enable DHT for magnet links + setEnableDht(true) + // Enable Local Service Discovery + setEnableLsd(true) + // User agent + setString(org.libtorrent4j.swig.settings_pack.string_types.user_agent.swigValue(), "NeoMovies/1.0 libtorrent4j/2.1.0") } + + private val sessionParams = SessionParams(settingsPack) init { startSession() @@ -60,7 +65,7 @@ class TorrentEngine private constructor(private val context: Context) { private fun startSession() { try { session = SessionManager() - session?.start(settings) + session?.start(sessionParams) isSessionStarted = true Log.d(TAG, "LibTorrent session started") } catch (e: Exception) { @@ -93,21 +98,21 @@ class TorrentEngine private constructor(private val context: Context) { * Start alert listener for torrent events */ private fun startAlertListener() { - scope.launch { - while (isActive && isSessionStarted) { - try { - session?.let { sess -> - val alerts = sess.popAlerts() - for (alert in alerts) { - handleAlert(alert) - } - } - delay(1000) // Check every second - } catch (e: Exception) { - Log.e(TAG, "Error in alert listener", e) - } + session?.addListener(object : AlertListener { + override fun types(): IntArray { + return intArrayOf( + AlertType.METADATA_RECEIVED.swig(), + AlertType.TORRENT_FINISHED.swig(), + AlertType.TORRENT_ERROR.swig(), + AlertType.STATE_CHANGED.swig(), + AlertType.TORRENT_CHECKED.swig() + ) } - } + + override fun alert(alert: Alert<*>) { + handleAlert(alert) + } + }) } /** @@ -191,7 +196,8 @@ class TorrentEngine private constructor(private val context: Context) { scope.launch { val handle = alert.handle() val infoHash = handle.infoHash().toHex() - val error = alert.error().message() + // message is a property in Kotlin + val error = alert.error().message Log.e(TAG, "Torrent error: $infoHash - $error") torrentDao.setTorrentError(infoHash, error) @@ -205,7 +211,8 @@ class TorrentEngine private constructor(private val context: Context) { scope.launch { val handle = alert.handle() val infoHash = handle.infoHash().toHex() - val state = when (alert.state()) { + val status = handle.status() + val state = when (status.state()) { TorrentStatus.State.CHECKING_FILES -> TorrentState.CHECKING TorrentStatus.State.DOWNLOADING_METADATA -> TorrentState.METADATA_DOWNLOADING TorrentStatus.State.DOWNLOADING -> TorrentState.DOWNLOADING @@ -251,15 +258,11 @@ class TorrentEngine private constructor(private val context: Context) { ): String { return withContext(Dispatchers.IO) { try { - // Parse magnet URI - val error = ErrorCode() - val params = SessionHandle.parseMagnetUri(magnetUri, error) + // Parse magnet URI using new API + val params = AddTorrentParams.parseMagnetUri(magnetUri) - if (error.value() != 0) { - throw Exception("Invalid magnet URI: ${error.message()}") - } - - val infoHash = params.infoHash().toHex() + // Get info hash from parsed params - best is a property + val infoHash = params.infoHashes.best.toHex() // Check if already exists val existing = existingTorrent ?: torrentDao.getTorrent(infoHash) @@ -268,22 +271,16 @@ class TorrentEngine private constructor(private val context: Context) { return@withContext infoHash } - // Set save path + // Set save path and apply to params val saveDir = File(savePath) if (!saveDir.exists()) { saveDir.mkdirs() } - params.savePath(saveDir.absolutePath) + params.swig().setSave_path(saveDir.absolutePath) - // Add to session - val handle = session?.swig()?.addTorrent(params, error) - ?: throw Exception("Session not initialized") - - if (error.value() != 0) { - throw Exception("Failed to add torrent: ${error.message()}") - } - - torrentHandles[infoHash] = TorrentHandle(handle) + // Add to session using async API + // Handle will be received asynchronously via ADD_TORRENT alert + session?.swig()?.async_add_torrent(params.swig()) ?: throw Exception("Session not initialized") // Save to database val torrentInfo = TorrentInfo( @@ -334,9 +331,11 @@ class TorrentEngine private constructor(private val context: Context) { Log.d(TAG, "Torrent paused: $infoHash") // Stop service if no active torrents - if (torrentDao.getActiveTorrents().isEmpty()) { + val activeTorrents = torrentDao.getActiveTorrents() + if (activeTorrents.isEmpty()) { stopService() } + Unit // Explicitly return Unit } catch (e: Exception) { Log.e(TAG, "Failed to pause torrent", e) } @@ -372,9 +371,11 @@ class TorrentEngine private constructor(private val context: Context) { Log.d(TAG, "Torrent removed: $infoHash") // Stop service if no active torrents - if (torrentDao.getActiveTorrents().isEmpty()) { + val activeTorrents = torrentDao.getActiveTorrents() + if (activeTorrents.isEmpty()) { stopService() } + Unit // Explicitly return Unit } catch (e: Exception) { Log.e(TAG, "Failed to remove torrent", e) } @@ -393,7 +394,15 @@ class TorrentEngine private constructor(private val context: Context) { withContext(Dispatchers.IO) { try { val handle = torrentHandles[infoHash] ?: return@withContext - handle.filePriority(fileIndex, Priority.getValue(priority.value)) + // Convert FilePriority to LibTorrent Priority + val libPriority = when (priority) { + FilePriority.DONT_DOWNLOAD -> Priority.IGNORE + FilePriority.LOW -> Priority.LOW + FilePriority.NORMAL -> Priority.DEFAULT + FilePriority.HIGH -> Priority.TOP_PRIORITY + else -> Priority.DEFAULT // Default + } + handle.filePriority(fileIndex, libPriority) // Update database val torrent = torrentDao.getTorrent(infoHash) ?: return@withContext @@ -418,7 +427,14 @@ class TorrentEngine private constructor(private val context: Context) { val handle = torrentHandles[infoHash] ?: return@withContext priorities.forEach { (fileIndex, priority) -> - handle.filePriority(fileIndex, Priority.getValue(priority.value)) + val libPriority = when (priority) { + FilePriority.DONT_DOWNLOAD -> Priority.IGNORE + FilePriority.LOW -> Priority.LOW + FilePriority.NORMAL -> Priority.DEFAULT + FilePriority.HIGH -> Priority.TOP_PRIORITY + else -> Priority.DEFAULT // Default + } + handle.filePriority(fileIndex, libPriority) } // Update database