mirror of
https://gitlab.com/foxixus/neomovies_mobile.git
synced 2025-10-28 03:18:49 +05:00
v0.0.2
This commit is contained in:
304
CI_CD_README.md
304
CI_CD_README.md
@@ -1,304 +0,0 @@
|
|||||||
# 🚀 CI/CD Configuration для NeoMovies Mobile
|
|
||||||
|
|
||||||
## 📋 Обзор
|
|
||||||
|
|
||||||
Автоматическая сборка APK и TorrentEngine модуля с оптимизацией использования RAM.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Конфигурации
|
|
||||||
|
|
||||||
### 1. **GitLab CI/CD** (`.gitlab-ci.yml`)
|
|
||||||
|
|
||||||
Основная конфигурация для GitLab:
|
|
||||||
|
|
||||||
#### **Stages:**
|
|
||||||
- **build** - Сборка APK и AAR
|
|
||||||
- **test** - Анализ кода и тесты
|
|
||||||
- **deploy** - Публикация релизов
|
|
||||||
|
|
||||||
#### **Jobs:**
|
|
||||||
|
|
||||||
| Job | Описание | Артефакты | Ветки |
|
|
||||||
|-----|----------|-----------|-------|
|
|
||||||
| `build:torrent-engine` | Сборка TorrentEngine AAR | `*.aar` | dev, feature/*, MR |
|
|
||||||
| `build:apk-debug` | Сборка Debug APK | `app-debug.apk` | dev, feature/*, MR |
|
|
||||||
| `build:apk-release` | Сборка Release APK | `app-arm64-v8a-release.apk` | только dev |
|
|
||||||
| `test:flutter-analyze` | Анализ Dart кода | - | dev, MR |
|
|
||||||
| `test:android-lint` | Android Lint | HTML отчеты | dev, MR |
|
|
||||||
| `deploy:release` | Публикация релиза | - | только tags (manual) |
|
|
||||||
|
|
||||||
### 2. **GitHub Actions** (`.github/workflows/build.yml`)
|
|
||||||
|
|
||||||
Альтернативная конфигурация для GitHub:
|
|
||||||
|
|
||||||
#### **Workflows:**
|
|
||||||
|
|
||||||
| Workflow | Триггер | Описание |
|
|
||||||
|----------|---------|----------|
|
|
||||||
| `build-torrent-engine` | push, PR | Сборка AAR модуля |
|
|
||||||
| `build-debug-apk` | push, PR | Debug APK для тестирования |
|
|
||||||
| `build-release-apk` | push to dev | Release APK (split-per-abi) |
|
|
||||||
| `code-quality` | push, PR | Flutter analyze + Android Lint |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Оптимизация RAM
|
|
||||||
|
|
||||||
### **gradle.properties**
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# Уменьшено с 4GB до 2GB
|
|
||||||
org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G
|
|
||||||
|
|
||||||
# Kotlin daemon с ограничением
|
|
||||||
kotlin.daemon.jvmargs=-Xmx1G -XX:MaxMetaspaceSize=512m
|
|
||||||
|
|
||||||
# Включены оптимизации
|
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.caching=true
|
|
||||||
org.gradle.configureondemand=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### **CI переменные**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# В CI используется еще меньше RAM
|
|
||||||
GRADLE_OPTS="-Xmx1536m -XX:MaxMetaspaceSize=512m"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Артефакты
|
|
||||||
|
|
||||||
### **TorrentEngine AAR:**
|
|
||||||
- Путь: `android/torrentengine/build/outputs/aar/`
|
|
||||||
- Файл: `torrentengine-release.aar`
|
|
||||||
- Срок хранения: 7 дней
|
|
||||||
- Размер: ~5-10 MB
|
|
||||||
|
|
||||||
### **Debug APK:**
|
|
||||||
- Путь: `build/app/outputs/flutter-apk/`
|
|
||||||
- Файл: `app-debug.apk`
|
|
||||||
- Срок хранения: 7 дней
|
|
||||||
- Размер: ~50-80 MB
|
|
||||||
|
|
||||||
### **Release APK:**
|
|
||||||
- Путь: `build/app/outputs/flutter-apk/`
|
|
||||||
- Файл: `app-arm64-v8a-release.apk`
|
|
||||||
- Срок хранения: 30 дней
|
|
||||||
- Размер: ~30-50 MB (split-per-abi)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚦 Триггеры сборки
|
|
||||||
|
|
||||||
### **GitLab:**
|
|
||||||
|
|
||||||
**Автоматически запускается при:**
|
|
||||||
- Push в `dev` ветку
|
|
||||||
- Push в `feature/torrent-engine-integration`
|
|
||||||
- Создание Merge Request
|
|
||||||
- Push тега (для deploy)
|
|
||||||
|
|
||||||
**Ручной запуск:**
|
|
||||||
- Web UI → Pipelines → Run Pipeline
|
|
||||||
- Выбрать ветку и нажать "Run pipeline"
|
|
||||||
|
|
||||||
### **GitHub:**
|
|
||||||
|
|
||||||
**Автоматически запускается при:**
|
|
||||||
- Push в `dev` или `feature/torrent-engine-integration`
|
|
||||||
- Pull Request в `dev`
|
|
||||||
|
|
||||||
**Ручной запуск:**
|
|
||||||
- Actions → Build NeoMovies Mobile → Run workflow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Настройка GitLab Instance Runners
|
|
||||||
|
|
||||||
### **Рекомендуется: Использовать GitLab Instance Runners (SaaS)**
|
|
||||||
|
|
||||||
GitLab предоставляет 112+ бесплатных shared runners для всех проектов!
|
|
||||||
|
|
||||||
**Как включить:**
|
|
||||||
|
|
||||||
1. Перейдите в **Settings → CI/CD → Runners**
|
|
||||||
2. Найдите секцию **"Instance runners"**
|
|
||||||
3. Нажмите **"Enable instance runners for this project"**
|
|
||||||
4. Готово! ✅
|
|
||||||
|
|
||||||
**Доступные теги для Instance Runners:**
|
|
||||||
|
|
||||||
| Тег | RAM | CPU | Описание |
|
|
||||||
|-----|-----|-----|----------|
|
|
||||||
| `saas-linux-small-amd64` | 2 GB | 1 core | Легкие задачи |
|
|
||||||
| `saas-linux-medium-amd64` | 4 GB | 2 cores | **Рекомендуется для Android** |
|
|
||||||
| `saas-linux-large-amd64` | 8 GB | 4 cores | Тяжелые сборки |
|
|
||||||
| `docker` | varies | varies | Любой Docker runner |
|
|
||||||
|
|
||||||
**Наша конфигурация использует:**
|
|
||||||
- TorrentEngine: `saas-linux-medium-amd64` (4GB, 2 cores)
|
|
||||||
- Остальные jobs: `docker` (автоматический выбор)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Альтернатива: Локальный Runner (не требуется)**
|
|
||||||
|
|
||||||
Только если нужна кастомная конфигурация:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Установка GitLab Runner
|
|
||||||
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
|
|
||||||
sudo apt-get install gitlab-runner
|
|
||||||
|
|
||||||
# 2. Регистрация Runner
|
|
||||||
sudo gitlab-runner register \
|
|
||||||
--url https://gitlab.com/ \
|
|
||||||
--registration-token YOUR_TOKEN \
|
|
||||||
--executor docker \
|
|
||||||
--docker-image mingc/android-build-box:latest \
|
|
||||||
--tag-list docker,android
|
|
||||||
|
|
||||||
# 3. Запуск
|
|
||||||
sudo gitlab-runner start
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Время сборки (примерно)
|
|
||||||
|
|
||||||
| Job | Время | RAM | CPU |
|
|
||||||
|-----|-------|-----|-----|
|
|
||||||
| TorrentEngine | ~5-10 мин | 1.5GB | 2 cores |
|
|
||||||
| Debug APK | ~15-20 мин | 2GB | 2 cores |
|
|
||||||
| Release APK | ~20-30 мин | 2GB | 2 cores |
|
|
||||||
| Flutter Analyze | ~2-3 мин | 512MB | 1 core |
|
|
||||||
| Android Lint | ~5-8 мин | 1GB | 2 cores |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐳 Docker образы
|
|
||||||
|
|
||||||
### **mingc/android-build-box:latest**
|
|
||||||
|
|
||||||
Включает:
|
|
||||||
- Android SDK (latest)
|
|
||||||
- Flutter SDK
|
|
||||||
- Java 17
|
|
||||||
- Gradle
|
|
||||||
- Git, curl, wget
|
|
||||||
|
|
||||||
Размер: ~8GB
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Кэширование
|
|
||||||
|
|
||||||
Для ускорения сборок используется кэширование:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .gradle/ # Gradle dependencies
|
|
||||||
- .pub-cache/ # Flutter packages
|
|
||||||
- android/.gradle/ # Android build cache
|
|
||||||
- build/ # Flutter build cache
|
|
||||||
```
|
|
||||||
|
|
||||||
**Эффект:**
|
|
||||||
- Первая сборка: ~25 минут
|
|
||||||
- Последующие: ~10-15 минут (с кэшем)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Логи и отладка
|
|
||||||
|
|
||||||
### **Просмотр логов GitLab:**
|
|
||||||
|
|
||||||
1. Перейти в **CI/CD → Pipelines**
|
|
||||||
2. Выбрать pipeline
|
|
||||||
3. Кликнуть на job для просмотра логов
|
|
||||||
|
|
||||||
### **Отладка локально:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Тестирование сборки TorrentEngine
|
|
||||||
cd android
|
|
||||||
./gradlew :torrentengine:assembleRelease \
|
|
||||||
--no-daemon \
|
|
||||||
--parallel \
|
|
||||||
--stacktrace
|
|
||||||
|
|
||||||
# Тестирование Flutter APK
|
|
||||||
flutter build apk --debug --verbose
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Troubleshooting
|
|
||||||
|
|
||||||
### **Gradle daemon crashed:**
|
|
||||||
|
|
||||||
**Проблема:** `Gradle build daemon disappeared unexpectedly`
|
|
||||||
|
|
||||||
**Решение:**
|
|
||||||
```bash
|
|
||||||
# Увеличить RAM в gradle.properties
|
|
||||||
org.gradle.jvmargs=-Xmx3G
|
|
||||||
|
|
||||||
# Или отключить daemon
|
|
||||||
./gradlew --no-daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Out of memory:**
|
|
||||||
|
|
||||||
**Проблема:** `OutOfMemoryError: Java heap space`
|
|
||||||
|
|
||||||
**Решение:**
|
|
||||||
```bash
|
|
||||||
# Увеличить heap в CI
|
|
||||||
GRADLE_OPTS="-Xmx2048m -XX:MaxMetaspaceSize=768m"
|
|
||||||
```
|
|
||||||
|
|
||||||
### **LibTorrent4j native libraries not found:**
|
|
||||||
|
|
||||||
**Проблема:** Нативные библиотеки не найдены
|
|
||||||
|
|
||||||
**Решение:**
|
|
||||||
- Убедиться что все архитектуры включены в `build.gradle.kts`
|
|
||||||
- Проверить `splits.abi` конфигурацию
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Дополнительные ресурсы
|
|
||||||
|
|
||||||
- [GitLab CI/CD Docs](https://docs.gitlab.com/ee/ci/)
|
|
||||||
- [GitHub Actions Docs](https://docs.github.com/actions)
|
|
||||||
- [Flutter CI/CD Guide](https://docs.flutter.dev/deployment/cd)
|
|
||||||
- [Gradle Performance](https://docs.gradle.org/current/userguide/performance.html)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Следующие шаги
|
|
||||||
|
|
||||||
1. **Настроить GitLab Runner** (если еще не настроен)
|
|
||||||
2. **Запушить изменения** в dev ветку
|
|
||||||
3. **Проверить Pipeline** в GitLab CI/CD
|
|
||||||
4. **Скачать артефакты** после успешной сборки
|
|
||||||
5. **Протестировать APK** на реальном устройстве
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Поддержка
|
|
||||||
|
|
||||||
При проблемах с CI/CD:
|
|
||||||
1. Проверьте логи pipeline
|
|
||||||
2. Убедитесь что Runner активен
|
|
||||||
3. Проверьте доступность Docker образа
|
|
||||||
4. Создайте issue с логами ошибки
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Создано с ❤️ для NeoMovies Mobile**
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
# 📝 Development Summary - NeoMovies Mobile
|
|
||||||
|
|
||||||
## 🎯 Выполненные задачи
|
|
||||||
|
|
||||||
### 1. ⚡ Торрент Движок (TorrentEngine Library)
|
|
||||||
|
|
||||||
Создана **полноценная библиотека для работы с торрентами** как отдельный модуль Android:
|
|
||||||
|
|
||||||
#### 📦 Структура модуля:
|
|
||||||
```
|
|
||||||
android/torrentengine/
|
|
||||||
├── build.gradle.kts # Конфигурация с LibTorrent4j
|
|
||||||
├── proguard-rules.pro # ProGuard правила
|
|
||||||
├── consumer-rules.pro # Consumer ProGuard rules
|
|
||||||
├── README.md # Подробная документация
|
|
||||||
└── src/main/
|
|
||||||
├── AndroidManifest.xml # Permissions и Service
|
|
||||||
└── java/com/neomovies/torrentengine/
|
|
||||||
├── TorrentEngine.kt # Главный API класс
|
|
||||||
├── models/
|
|
||||||
│ └── TorrentInfo.kt # Модели данных (TorrentInfo, TorrentFile, etc.)
|
|
||||||
├── database/
|
|
||||||
│ ├── TorrentDao.kt # Room DAO
|
|
||||||
│ ├── TorrentDatabase.kt
|
|
||||||
│ └── Converters.kt # Type converters
|
|
||||||
└── service/
|
|
||||||
└── TorrentService.kt # Foreground service
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✨ Возможности TorrentEngine:
|
|
||||||
|
|
||||||
1. **Загрузка из magnet-ссылок**
|
|
||||||
- Автоматическое получение метаданных
|
|
||||||
- Парсинг файлов и их размеров
|
|
||||||
- Поддержка DHT и LSD
|
|
||||||
|
|
||||||
2. **Управление файлами**
|
|
||||||
- Выбор файлов ДО начала загрузки
|
|
||||||
- Изменение приоритетов В ПРОЦЕССЕ загрузки
|
|
||||||
- Фильтрация по типу (видео, аудио и т.д.)
|
|
||||||
- 5 уровней приоритета: DONT_DOWNLOAD, LOW, NORMAL, HIGH, MAXIMUM
|
|
||||||
|
|
||||||
3. **Foreground Service с уведомлением**
|
|
||||||
- Постоянное уведомление (не удаляется пока активны торренты)
|
|
||||||
- Отображение скорости загрузки/отдачи
|
|
||||||
- Список активных торрентов с прогрессом
|
|
||||||
- Кнопки управления (Pause All)
|
|
||||||
|
|
||||||
4. **Персистентность (Room Database)**
|
|
||||||
- Автоматическое сохранение состояния
|
|
||||||
- Восстановление торрентов после перезагрузки
|
|
||||||
- Реактивные Flow для мониторинга изменений
|
|
||||||
|
|
||||||
5. **Полная статистика**
|
|
||||||
- Скорость загрузки/отдачи (real-time)
|
|
||||||
- Количество пиров и сидов
|
|
||||||
- Прогресс загрузки (%)
|
|
||||||
- ETA (время до завершения)
|
|
||||||
- Share ratio (отдано/скачано)
|
|
||||||
|
|
||||||
6. **Контроль раздач**
|
|
||||||
- `addTorrent()` - добавить торрент
|
|
||||||
- `pauseTorrent()` - поставить на паузу
|
|
||||||
- `resumeTorrent()` - возобновить
|
|
||||||
- `removeTorrent()` - удалить (с файлами или без)
|
|
||||||
- `setFilePriority()` - изменить приоритет файла
|
|
||||||
- `setFilePriorities()` - массовое изменение приоритетов
|
|
||||||
|
|
||||||
#### 📚 Использование:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// Инициализация
|
|
||||||
val torrentEngine = TorrentEngine.getInstance(context)
|
|
||||||
torrentEngine.startStatsUpdater()
|
|
||||||
|
|
||||||
// Добавление торрента
|
|
||||||
val infoHash = torrentEngine.addTorrent(magnetUri, savePath)
|
|
||||||
|
|
||||||
// Мониторинг (реактивно)
|
|
||||||
torrentEngine.getAllTorrentsFlow().collect { torrents ->
|
|
||||||
torrents.forEach { torrent ->
|
|
||||||
println("${torrent.name}: ${torrent.progress * 100}%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Изменение приоритетов файлов
|
|
||||||
torrent.files.forEachIndexed { index, file ->
|
|
||||||
if (file.isVideo()) {
|
|
||||||
torrentEngine.setFilePriority(infoHash, index, FilePriority.HIGH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Управление
|
|
||||||
torrentEngine.pauseTorrent(infoHash)
|
|
||||||
torrentEngine.resumeTorrent(infoHash)
|
|
||||||
torrentEngine.removeTorrent(infoHash, deleteFiles = true)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 🔄 Новый API Client (NeoMoviesApiClient)
|
|
||||||
|
|
||||||
Полностью переписан API клиент для работы с **новым Go-based бэкендом (neomovies-api)**:
|
|
||||||
|
|
||||||
#### 📍 Файл: `lib/data/api/neomovies_api_client.dart`
|
|
||||||
|
|
||||||
#### 🆕 Новые возможности:
|
|
||||||
|
|
||||||
**Аутентификация:**
|
|
||||||
- ✅ `register()` - регистрация с отправкой кода на email
|
|
||||||
- ✅ `verifyEmail()` - подтверждение email кодом
|
|
||||||
- ✅ `resendVerificationCode()` - повторная отправка кода
|
|
||||||
- ✅ `login()` - вход по email/password
|
|
||||||
- ✅ `getGoogleOAuthUrl()` - URL для Google OAuth
|
|
||||||
- ✅ `refreshToken()` - обновление JWT токена
|
|
||||||
- ✅ `getProfile()` - получение профиля
|
|
||||||
- ✅ `deleteAccount()` - удаление аккаунта
|
|
||||||
|
|
||||||
**Фильмы:**
|
|
||||||
- ✅ `getPopularMovies()` - популярные фильмы
|
|
||||||
- ✅ `getTopRatedMovies()` - топ рейтинг
|
|
||||||
- ✅ `getUpcomingMovies()` - скоро выйдут
|
|
||||||
- ✅ `getNowPlayingMovies()` - сейчас в кино
|
|
||||||
- ✅ `getMovieById()` - детали фильма
|
|
||||||
- ✅ `getMovieRecommendations()` - рекомендации
|
|
||||||
- ✅ `searchMovies()` - поиск фильмов
|
|
||||||
|
|
||||||
**Сериалы:**
|
|
||||||
- ✅ `getPopularTvShows()` - популярные сериалы
|
|
||||||
- ✅ `getTopRatedTvShows()` - топ сериалы
|
|
||||||
- ✅ `getTvShowById()` - детали сериала
|
|
||||||
- ✅ `getTvShowRecommendations()` - рекомендации
|
|
||||||
- ✅ `searchTvShows()` - поиск сериалов
|
|
||||||
|
|
||||||
**Избранное:**
|
|
||||||
- ✅ `getFavorites()` - список избранного
|
|
||||||
- ✅ `addFavorite()` - добавить в избранное
|
|
||||||
- ✅ `removeFavorite()` - удалить из избранного
|
|
||||||
|
|
||||||
**Реакции (новое!):**
|
|
||||||
- ✅ `getReactionCounts()` - количество лайков/дизлайков
|
|
||||||
- ✅ `setReaction()` - поставить like/dislike
|
|
||||||
- ✅ `getMyReactions()` - мои реакции
|
|
||||||
|
|
||||||
**Торренты (новое!):**
|
|
||||||
- ✅ `searchTorrents()` - поиск торрентов через RedAPI
|
|
||||||
- По IMDb ID
|
|
||||||
- Фильтры: quality, season, episode
|
|
||||||
- Поддержка фильмов и сериалов
|
|
||||||
|
|
||||||
**Плееры (новое!):**
|
|
||||||
- ✅ `getAllohaPlayer()` - Alloha embed URL
|
|
||||||
- ✅ `getLumexPlayer()` - Lumex embed URL
|
|
||||||
- ✅ `getVibixPlayer()` - Vibix embed URL
|
|
||||||
|
|
||||||
#### 🔧 Пример использования:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final apiClient = NeoMoviesApiClient(http.Client());
|
|
||||||
|
|
||||||
// Регистрация с email verification
|
|
||||||
await apiClient.register(
|
|
||||||
email: 'user@example.com',
|
|
||||||
password: 'password123',
|
|
||||||
name: 'John Doe',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Подтверждение кода
|
|
||||||
final authResponse = await apiClient.verifyEmail(
|
|
||||||
email: 'user@example.com',
|
|
||||||
code: '123456',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Поиск торрентов
|
|
||||||
final torrents = await apiClient.searchTorrents(
|
|
||||||
imdbId: 'tt1234567',
|
|
||||||
type: 'movie',
|
|
||||||
quality: '1080p',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Получить плеер
|
|
||||||
final player = await apiClient.getAllohaPlayer('tt1234567');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 📊 Новые модели данных
|
|
||||||
|
|
||||||
Созданы модели для новых фич:
|
|
||||||
|
|
||||||
#### `PlayerResponse` (`lib/data/models/player/player_response.dart`):
|
|
||||||
```dart
|
|
||||||
class PlayerResponse {
|
|
||||||
final String? embedUrl;
|
|
||||||
final String? playerType;
|
|
||||||
final String? error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 📖 Документация
|
|
||||||
|
|
||||||
Создана подробная документация:
|
|
||||||
- **`android/torrentengine/README.md`** - полное руководство по TorrentEngine
|
|
||||||
- Описание всех возможностей
|
|
||||||
- Примеры использования
|
|
||||||
- API reference
|
|
||||||
- Интеграция с Flutter
|
|
||||||
- Известные проблемы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Что готово к использованию
|
|
||||||
|
|
||||||
### ✅ TorrentEngine Library
|
|
||||||
- Полностью функциональный торрент движок
|
|
||||||
- Можно использовать как отдельную библиотеку
|
|
||||||
- Готов к интеграции с Flutter через MethodChannel
|
|
||||||
- Все основные функции реализованы
|
|
||||||
|
|
||||||
### ✅ NeoMoviesApiClient
|
|
||||||
- Полная поддержка нового API
|
|
||||||
- Все endpoints реализованы
|
|
||||||
- Готов к замене старого ApiClient
|
|
||||||
|
|
||||||
### ✅ База для дальнейшей разработки
|
|
||||||
- Структура модуля torrentengine создана
|
|
||||||
- Build конфигурация готова
|
|
||||||
- ProGuard правила настроены
|
|
||||||
- Permissions объявлены
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Следующие шаги
|
|
||||||
|
|
||||||
### 1. Интеграция TorrentEngine с Flutter
|
|
||||||
|
|
||||||
Создать MethodChannel в `MainActivity.kt`:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
private val TORRENT_CHANNEL = "com.neomovies/torrent"
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
||||||
super.configureFlutterEngine(flutterEngine)
|
|
||||||
|
|
||||||
val torrentEngine = TorrentEngine.getInstance(applicationContext)
|
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, TORRENT_CHANNEL)
|
|
||||||
.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"addTorrent" -> {
|
|
||||||
val magnetUri = call.argument<String>("magnetUri")!!
|
|
||||||
val savePath = call.argument<String>("savePath")!!
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val hash = torrentEngine.addTorrent(magnetUri, savePath)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
result.success(hash)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
result.error("ERROR", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"getTorrents" -> {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val torrents = torrentEngine.getAllTorrents()
|
|
||||||
val torrentsJson = torrents.map { /* convert to map */ }
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
result.success(torrentsJson)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
result.error("ERROR", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ... другие методы
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Создать Dart wrapper:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class TorrentEngineService {
|
|
||||||
static const platform = MethodChannel('com.neomovies/torrent');
|
|
||||||
|
|
||||||
Future<String> addTorrent(String magnetUri, String savePath) async {
|
|
||||||
return await platform.invokeMethod('addTorrent', {
|
|
||||||
'magnetUri': magnetUri,
|
|
||||||
'savePath': savePath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getTorrents() async {
|
|
||||||
final List<dynamic> result = await platform.invokeMethod('getTorrents');
|
|
||||||
return result.cast<Map<String, dynamic>>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Замена старого API клиента
|
|
||||||
|
|
||||||
В файлах сервисов и репозиториев заменить:
|
|
||||||
```dart
|
|
||||||
// Старое
|
|
||||||
final apiClient = ApiClient(http.Client());
|
|
||||||
|
|
||||||
// Новое
|
|
||||||
final apiClient = NeoMoviesApiClient(http.Client());
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Создание UI для новых фич
|
|
||||||
|
|
||||||
**Email Verification Screen:**
|
|
||||||
- Ввод кода подтверждения
|
|
||||||
- Кнопка "Отправить код повторно"
|
|
||||||
- Таймер обратного отсчета
|
|
||||||
|
|
||||||
**Torrent List Screen:**
|
|
||||||
- Список активных торрентов
|
|
||||||
- Прогресс бар для каждого
|
|
||||||
- Скорость загрузки/отдачи
|
|
||||||
- Кнопки pause/resume/delete
|
|
||||||
|
|
||||||
**File Selection Screen:**
|
|
||||||
- Список файлов в торренте
|
|
||||||
- Checkbox для выбора файлов
|
|
||||||
- Slider для приоритета
|
|
||||||
- Отображение размера файлов
|
|
||||||
|
|
||||||
**Player Selection Screen:**
|
|
||||||
- Выбор плеера (Alloha/Lumex/Vibix)
|
|
||||||
- WebView для отображения плеера
|
|
||||||
|
|
||||||
**Reactions UI:**
|
|
||||||
- Кнопки like/dislike
|
|
||||||
- Счетчики реакций
|
|
||||||
- Анимации при клике
|
|
||||||
|
|
||||||
### 4. Тестирование
|
|
||||||
|
|
||||||
1. **Компиляция проекта:**
|
|
||||||
```bash
|
|
||||||
cd neomovies_mobile
|
|
||||||
flutter pub get
|
|
||||||
flutter build apk --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Тестирование TorrentEngine:**
|
|
||||||
- Добавление magnet-ссылки
|
|
||||||
- Получение метаданных
|
|
||||||
- Выбор файлов
|
|
||||||
- Изменение приоритетов в процессе загрузки
|
|
||||||
- Проверка уведомления
|
|
||||||
- Pause/Resume/Delete
|
|
||||||
|
|
||||||
3. **Тестирование API:**
|
|
||||||
- Регистрация и email verification
|
|
||||||
- Логин
|
|
||||||
- Поиск торрентов
|
|
||||||
- Получение плееров
|
|
||||||
- Реакции
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Преимущества нового решения
|
|
||||||
|
|
||||||
### TorrentEngine:
|
|
||||||
✅ Отдельная библиотека - можно использовать в других проектах
|
|
||||||
✅ LibTorrent4j - надежный и производительный
|
|
||||||
✅ Foreground service - стабильная работа в фоне
|
|
||||||
✅ Room database - надежное хранение состояния
|
|
||||||
✅ Flow API - реактивные обновления UI
|
|
||||||
✅ Полный контроль - все функции доступны
|
|
||||||
|
|
||||||
### NeoMoviesApiClient:
|
|
||||||
✅ Go backend - в 3x быстрее Node.js
|
|
||||||
✅ Меньше потребление памяти - 50% экономия
|
|
||||||
✅ Email verification - безопасная регистрация
|
|
||||||
✅ Google OAuth - удобный вход
|
|
||||||
✅ Торрент поиск - интеграция с RedAPI
|
|
||||||
✅ Множество плееров - выбор для пользователя
|
|
||||||
✅ Реакции - вовлечение пользователей
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Итоги
|
|
||||||
|
|
||||||
**Создано:**
|
|
||||||
- ✅ Полноценная библиотека TorrentEngine (700+ строк кода)
|
|
||||||
- ✅ Новый API клиент NeoMoviesApiClient (450+ строк)
|
|
||||||
- ✅ Модели данных для новых фич
|
|
||||||
- ✅ Подробная документация
|
|
||||||
- ✅ ProGuard правила
|
|
||||||
- ✅ Готовая структура для интеграции
|
|
||||||
|
|
||||||
**Готово к:**
|
|
||||||
- ⚡ Компиляции и тестированию
|
|
||||||
- 📱 Интеграции с Flutter
|
|
||||||
- 🚀 Деплою в production
|
|
||||||
|
|
||||||
**Следующий шаг:**
|
|
||||||
Интеграция TorrentEngine с Flutter через MethodChannel и создание UI для торрент менеджера.
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# 🚀 Add TorrentEngine Library and New API Client
|
|
||||||
|
|
||||||
## 📝 Описание
|
|
||||||
|
|
||||||
Полная реализация торрент движка на Kotlin с использованием LibTorrent4j и интеграция с Flutter приложением через MethodChannel. Также добавлен новый API клиент для работы с обновленным Go-based бэкендом.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Новые возможности
|
|
||||||
|
|
||||||
### 1. **TorrentEngine Library** (Kotlin)
|
|
||||||
|
|
||||||
Полноценный торрент движок как отдельный модуль Android:
|
|
||||||
|
|
||||||
#### 🎯 **Основные функции:**
|
|
||||||
- ✅ Загрузка из magnet-ссылок с автоматическим извлечением метаданных
|
|
||||||
- ✅ Выбор файлов ДО и ВО ВРЕМЯ загрузки
|
|
||||||
- ✅ Управление приоритетами файлов (5 уровней: DONT_DOWNLOAD → MAXIMUM)
|
|
||||||
- ✅ Foreground Service с постоянным уведомлением
|
|
||||||
- ✅ Room Database для персистентности состояния
|
|
||||||
- ✅ Реактивные Flow API для мониторинга изменений
|
|
||||||
- ✅ Полная статистика (скорость, пиры, сиды, прогресс, ETA)
|
|
||||||
- ✅ Pause/Resume/Remove с опциональным удалением файлов
|
|
||||||
|
|
||||||
#### 📦 **Структура модуля:**
|
|
||||||
```
|
|
||||||
android/torrentengine/
|
|
||||||
├── TorrentEngine.kt # Главный API класс (500+ строк)
|
|
||||||
├── TorrentService.kt # Foreground service с уведомлением
|
|
||||||
├── models/TorrentInfo.kt # Модели данных
|
|
||||||
├── database/ # Room DAO и Database
|
|
||||||
│ ├── TorrentDao.kt
|
|
||||||
│ ├── TorrentDatabase.kt
|
|
||||||
│ └── Converters.kt
|
|
||||||
├── build.gradle.kts # LibTorrent4j dependencies
|
|
||||||
├── AndroidManifest.xml # Permissions и Service
|
|
||||||
├── README.md # Полная документация
|
|
||||||
└── proguard-rules.pro # ProGuard правила
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 🔧 **Использование:**
|
|
||||||
```kotlin
|
|
||||||
val engine = TorrentEngine.getInstance(context)
|
|
||||||
val hash = engine.addTorrent(magnetUri, savePath)
|
|
||||||
engine.setFilePriority(hash, fileIndex, FilePriority.HIGH)
|
|
||||||
engine.pauseTorrent(hash)
|
|
||||||
engine.resumeTorrent(hash)
|
|
||||||
engine.removeTorrent(hash, deleteFiles = true)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **MethodChannel Integration** (Kotlin ↔ Flutter)
|
|
||||||
|
|
||||||
Полная интеграция TorrentEngine с Flutter через MethodChannel в `MainActivity.kt`:
|
|
||||||
|
|
||||||
#### 📡 **Доступные методы:**
|
|
||||||
- `addTorrent(magnetUri, savePath)` → infoHash
|
|
||||||
- `getTorrents()` → List<TorrentInfo> (JSON)
|
|
||||||
- `getTorrent(infoHash)` → TorrentInfo (JSON)
|
|
||||||
- `pauseTorrent(infoHash)` → success
|
|
||||||
- `resumeTorrent(infoHash)` → success
|
|
||||||
- `removeTorrent(infoHash, deleteFiles)` → success
|
|
||||||
- `setFilePriority(infoHash, fileIndex, priority)` → success
|
|
||||||
|
|
||||||
### 3. **NeoMoviesApiClient** (Dart)
|
|
||||||
|
|
||||||
Новый API клиент для работы с Go-based бэкендом:
|
|
||||||
|
|
||||||
#### 🆕 **Новые endpoints:**
|
|
||||||
|
|
||||||
**Аутентификация:**
|
|
||||||
- Email verification flow (register → verify → login)
|
|
||||||
- Google OAuth URL
|
|
||||||
- Token refresh
|
|
||||||
|
|
||||||
**Торренты:**
|
|
||||||
- Поиск через RedAPI по IMDb ID
|
|
||||||
- Фильтры по качеству, сезону, эпизоду
|
|
||||||
|
|
||||||
**Плееры:**
|
|
||||||
- Alloha, Lumex, Vibix embed URLs
|
|
||||||
|
|
||||||
**Реакции:**
|
|
||||||
- Лайки/дизлайки
|
|
||||||
- Счетчики реакций
|
|
||||||
- Мои реакции
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Измененные файлы
|
|
||||||
|
|
||||||
### Android:
|
|
||||||
- `android/settings.gradle.kts` - добавлен модуль `:torrentengine`
|
|
||||||
- `android/app/build.gradle.kts` - обновлены зависимости, Java 17
|
|
||||||
- `android/app/src/main/kotlin/.../MainActivity.kt` - интеграция TorrentEngine
|
|
||||||
|
|
||||||
### Flutter:
|
|
||||||
- `pubspec.yaml` - исправлен конфликт `build_runner`
|
|
||||||
- `lib/data/api/neomovies_api_client.dart` - новый API клиент (450+ строк)
|
|
||||||
- `lib/data/models/player/player_response.dart` - модель ответа плеера
|
|
||||||
|
|
||||||
### Документация:
|
|
||||||
- `android/torrentengine/README.md` - подробная документация по TorrentEngine
|
|
||||||
- `DEVELOPMENT_SUMMARY.md` - полный отчет о проделанной работе
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Технические детали
|
|
||||||
|
|
||||||
### Зависимости:
|
|
||||||
|
|
||||||
**TorrentEngine:**
|
|
||||||
- LibTorrent4j 2.1.0-28 (arm64, arm, x86, x86_64)
|
|
||||||
- Room 2.6.1
|
|
||||||
- Kotlin Coroutines 1.9.0
|
|
||||||
- Gson 2.11.0
|
|
||||||
|
|
||||||
**App:**
|
|
||||||
- Обновлен Java до версии 17
|
|
||||||
- Обновлены AndroidX библиотеки
|
|
||||||
- Исправлен конфликт build_runner (2.4.13)
|
|
||||||
|
|
||||||
### Permissions:
|
|
||||||
- INTERNET, ACCESS_NETWORK_STATE
|
|
||||||
- WRITE/READ_EXTERNAL_STORAGE
|
|
||||||
- MANAGE_EXTERNAL_STORAGE (Android 11+)
|
|
||||||
- FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC
|
|
||||||
- POST_NOTIFICATIONS
|
|
||||||
- WAKE_LOCK
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Что работает
|
|
||||||
|
|
||||||
✅ **Структура TorrentEngine модуля создана**
|
|
||||||
✅ **LibTorrent4j интегрирован**
|
|
||||||
✅ **Room database настроена**
|
|
||||||
✅ **Foreground Service реализован**
|
|
||||||
✅ **MethodChannel для Flutter готов**
|
|
||||||
✅ **Новый API клиент написан**
|
|
||||||
✅ **Все файлы закоммичены и запушены**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Следующие шаги
|
|
||||||
|
|
||||||
### Для полного завершения требуется:
|
|
||||||
|
|
||||||
1. **Сборка APK** - необходима более мощная среда для полной компиляции с LibTorrent4j
|
|
||||||
2. **Flutter интеграция** - создать Dart wrapper для MethodChannel
|
|
||||||
3. **UI для торрентов** - экраны списка торрентов, выбора файлов
|
|
||||||
4. **Тестирование** - проверка работы на реальном устройстве
|
|
||||||
|
|
||||||
### Дополнительно:
|
|
||||||
- Исправить ошибки анализатора Dart (отсутствующие модели плеера)
|
|
||||||
- Сгенерировать код для `player_response.g.dart`
|
|
||||||
- Добавить модель `TorrentItem` для API клиента
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Статистика
|
|
||||||
|
|
||||||
- **Создано файлов:** 16
|
|
||||||
- **Изменено файлов:** 4
|
|
||||||
- **Добавлено строк кода:** ~2700+
|
|
||||||
- **Kotlin код:** ~1500 строк
|
|
||||||
- **Dart код:** ~500 строк
|
|
||||||
- **Документация:** ~700 строк
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Итоги
|
|
||||||
|
|
||||||
Создана **полноценная библиотека для работы с торрентами**, которая:
|
|
||||||
- Может использоваться как отдельный модуль в любых Android проектах
|
|
||||||
- Предоставляет все необходимые функции для торрент-клиента
|
|
||||||
- Интегрирована с Flutter через MethodChannel
|
|
||||||
- Имеет подробную документацию с примерами
|
|
||||||
|
|
||||||
Также создан **новый API клиент** для работы с обновленным бэкендом с поддержкой новых фич:
|
|
||||||
- Email verification
|
|
||||||
- Google OAuth
|
|
||||||
- Torrent search
|
|
||||||
- Multiple players
|
|
||||||
- Reactions system
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Ссылки
|
|
||||||
|
|
||||||
- **Branch:** `feature/torrent-engine-integration`
|
|
||||||
- **Commit:** 1b28c5d
|
|
||||||
- **Документация:** `android/torrentengine/README.md`
|
|
||||||
- **Отчет:** `DEVELOPMENT_SUMMARY.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 👤 Author
|
|
||||||
|
|
||||||
**Droid (Factory AI Assistant)**
|
|
||||||
|
|
||||||
Создано с использованием LibTorrent4j, Room, Kotlin Coroutines, и Flutter MethodChannel.
|
|
||||||
201
android/torrentengine/LICENSE
Normal file
201
android/torrentengine/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025 NeoMovies
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,20 +1,8 @@
|
|||||||
# TorrentEngine Library
|
# TorrentEngine Library
|
||||||
|
|
||||||
Мощная библиотека для Android, обеспечивающая полноценную работу с торрентами через LibTorrent4j.
|
Либа для моего клиента и других независимых проектов где нужен простой торрент движок.
|
||||||
|
|
||||||
## 🎯 Возможности
|
## Установка
|
||||||
|
|
||||||
- ✅ **Загрузка из magnet-ссылок** - получение метаданных и загрузка файлов
|
|
||||||
- ✅ **Выбор файлов** - возможность выбирать какие файлы загружать до и во время загрузки
|
|
||||||
- ✅ **Управление приоритетами** - изменение приоритета файлов в активной раздаче
|
|
||||||
- ✅ **Фоновый сервис** - непрерывная работа в фоне с foreground уведомлением
|
|
||||||
- ✅ **Постоянное уведомление** - нельзя закрыть пока активны загрузки
|
|
||||||
- ✅ **Персистентность** - сохранение состояния в Room database
|
|
||||||
- ✅ **Реактивность** - Flow API для мониторинга изменений
|
|
||||||
- ✅ **Полная статистика** - скорость, пиры, сиды, прогресс, ETA
|
|
||||||
- ✅ **Pause/Resume/Remove** - полный контроль над раздачами
|
|
||||||
|
|
||||||
## 📦 Установка
|
|
||||||
|
|
||||||
### 1. Добавьте модуль в `settings.gradle.kts`:
|
### 1. Добавьте модуль в `settings.gradle.kts`:
|
||||||
|
|
||||||
@@ -38,7 +26,7 @@ dependencies {
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Использование
|
## Использование
|
||||||
|
|
||||||
### Инициализация
|
### Инициализация
|
||||||
|
|
||||||
@@ -127,7 +115,7 @@ lifecycleScope.launch {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Модели данных
|
## Модели данных
|
||||||
|
|
||||||
### TorrentInfo
|
### TorrentInfo
|
||||||
|
|
||||||
@@ -180,7 +168,7 @@ enum class FilePriority(val value: Int) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔔 Foreground Service
|
## Foreground Service
|
||||||
|
|
||||||
Сервис автоматически запускается при добавлении торрента и показывает постоянное уведомление с:
|
Сервис автоматически запускается при добавлении торрента и показывает постоянное уведомление с:
|
||||||
- Количеством активных торрентов
|
- Количеством активных торрентов
|
||||||
@@ -190,12 +178,10 @@ enum class FilePriority(val value: Int) {
|
|||||||
|
|
||||||
Уведомление **нельзя закрыть** пока есть активные торренты.
|
Уведомление **нельзя закрыть** пока есть активные торренты.
|
||||||
|
|
||||||
## 💾 Персистентность
|
## Персистентность
|
||||||
|
|
||||||
Все торренты сохраняются в Room database и автоматически восстанавливаются при перезапуске приложения.
|
Все торренты сохраняются в Room database и автоматически восстанавливаются при перезапуске приложения.
|
||||||
|
|
||||||
## 🔧 Расширенные возможности
|
|
||||||
|
|
||||||
### Проверка видео файлов
|
### Проверка видео файлов
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -215,54 +201,6 @@ val selectedCount = torrent.getSelectedFilesCount()
|
|||||||
val selectedSize = torrent.getSelectedSize()
|
val selectedSize = torrent.getSelectedSize()
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📱 Интеграция с Flutter
|
[Apache License 2.0](LICENSE).
|
||||||
|
|
||||||
Создайте MethodChannel для вызова из Flutter:
|
Made with <3 by Erno/Foxix
|
||||||
|
|
||||||
```kotlin
|
|
||||||
class TorrentEngineChannel(private val context: Context) {
|
|
||||||
private val torrentEngine = TorrentEngine.getInstance(context)
|
|
||||||
private val channel = "com.neomovies/torrent"
|
|
||||||
|
|
||||||
fun setupMethodChannel(flutterEngine: FlutterEngine) {
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
|
|
||||||
.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"addTorrent" -> {
|
|
||||||
val magnetUri = call.argument<String>("magnetUri")!!
|
|
||||||
val savePath = call.argument<String>("savePath")!!
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
val hash = torrentEngine.addTorrent(magnetUri, savePath)
|
|
||||||
result.success(hash)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
result.error("ERROR", e.message, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ... другие методы
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 Лицензия
|
|
||||||
|
|
||||||
MIT License - используйте свободно в любых проектах!
|
|
||||||
|
|
||||||
## 🤝 Вклад
|
|
||||||
|
|
||||||
Библиотека разработана как универсальное решение для работы с торрентами в Android.
|
|
||||||
Может использоваться в любых проектах без ограничений.
|
|
||||||
|
|
||||||
## 🐛 Известные проблемы
|
|
||||||
|
|
||||||
- LibTorrent4j требует минимум Android 5.0 (API 21)
|
|
||||||
- Для Android 13+ нужно запрашивать POST_NOTIFICATIONS permission
|
|
||||||
- Foreground service требует отображения уведомления
|
|
||||||
|
|
||||||
## 📞 Поддержка
|
|
||||||
|
|
||||||
При возникновении проблем создайте issue с описанием и логами.
|
|
||||||
@@ -16,13 +16,7 @@ import java.io.File
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Main TorrentEngine class - the core of the torrent library
|
* Main TorrentEngine class - the core of the torrent library
|
||||||
* This is the main API that applications should use
|
* This is the main API that applications should use.
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```
|
|
||||||
* val engine = TorrentEngine.getInstance(context)
|
|
||||||
* engine.addTorrent(magnetUri, savePath)
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
class TorrentEngine private constructor(private val context: Context) {
|
class TorrentEngine private constructor(private val context: Context) {
|
||||||
private val TAG = "TorrentEngine"
|
private val TAG = "TorrentEngine"
|
||||||
|
|||||||
@@ -1,332 +1,110 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:neomovies_mobile/data/models/auth_response.dart';
|
|
||||||
import 'package:neomovies_mobile/data/models/favorite.dart';
|
|
||||||
import 'package:neomovies_mobile/data/models/movie.dart';
|
import 'package:neomovies_mobile/data/models/movie.dart';
|
||||||
|
import 'package:neomovies_mobile/data/models/favorite.dart';
|
||||||
import 'package:neomovies_mobile/data/models/reaction.dart';
|
import 'package:neomovies_mobile/data/models/reaction.dart';
|
||||||
|
import 'package:neomovies_mobile/data/models/auth_response.dart';
|
||||||
import 'package:neomovies_mobile/data/models/user.dart';
|
import 'package:neomovies_mobile/data/models/user.dart';
|
||||||
|
import 'package:neomovies_mobile/data/api/neomovies_api_client.dart'; // новый клиент
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final http.Client _client;
|
final NeoMoviesApiClient _neoClient;
|
||||||
final String _baseUrl = dotenv.env['API_URL']!;
|
|
||||||
|
|
||||||
ApiClient(this._client);
|
ApiClient(http.Client client)
|
||||||
|
: _neoClient = NeoMoviesApiClient(client);
|
||||||
|
|
||||||
Future<List<Movie>> getPopularMovies({int page = 1}) async {
|
// ---- Movies ----
|
||||||
return _fetchMovies('/movies/popular', page: page);
|
Future<List<Movie>> getPopularMovies({int page = 1}) {
|
||||||
|
return _neoClient.getPopularMovies(page: page);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Movie>> getTopRatedMovies({int page = 1}) async {
|
Future<List<Movie>> getTopRatedMovies({int page = 1}) {
|
||||||
return _fetchMovies('/movies/top-rated', page: page);
|
return _neoClient.getTopRatedMovies(page: page);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Movie>> getUpcomingMovies({int page = 1}) async {
|
Future<List<Movie>> getUpcomingMovies({int page = 1}) {
|
||||||
return _fetchMovies('/movies/upcoming', page: page);
|
return _neoClient.getUpcomingMovies(page: page);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Movie> getMovieById(String id) async {
|
Future<Movie> getMovieById(String id) {
|
||||||
return _fetchMovieDetail('/movies/$id');
|
return _neoClient.getMovieById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Movie> getTvById(String id) async {
|
Future<Movie> getTvById(String id) {
|
||||||
return _fetchMovieDetail('/tv/$id');
|
return _neoClient.getTvShowById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение IMDB ID для фильмов
|
// ---- Search ----
|
||||||
Future<String?> getMovieImdbId(int movieId) async {
|
Future<List<Movie>> searchMovies(String query, {int page = 1}) {
|
||||||
try {
|
return _neoClient.search(query, page: page);
|
||||||
final uri = Uri.parse('$_baseUrl/movies/$movieId/external-ids');
|
|
||||||
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final data = json.decode(response.body);
|
|
||||||
return data['imdb_id'] as String?;
|
|
||||||
} else {
|
|
||||||
print('Failed to get movie IMDB ID: ${response.statusCode}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error getting movie IMDB ID: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение IMDB ID для сериалов
|
// ---- Favorites ----
|
||||||
Future<String?> getTvImdbId(int showId) async {
|
Future<List<Favorite>> getFavorites() {
|
||||||
try {
|
return _neoClient.getFavorites();
|
||||||
final uri = Uri.parse('$_baseUrl/tv/$showId/external-ids');
|
|
||||||
final response = await _client.get(uri).timeout(const Duration(seconds: 30));
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final data = json.decode(response.body);
|
|
||||||
return data['imdb_id'] as String?;
|
|
||||||
} else {
|
|
||||||
print('Failed to get TV IMDB ID: ${response.statusCode}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error getting TV IMDB ID: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Универсальный метод получения IMDB ID
|
Future<void> addFavorite(
|
||||||
Future<String?> getImdbId(int mediaId, String mediaType) async {
|
String mediaId,
|
||||||
if (mediaType == 'tv') {
|
String mediaType,
|
||||||
return getTvImdbId(mediaId);
|
String title,
|
||||||
} else {
|
String posterPath,
|
||||||
return getMovieImdbId(mediaId);
|
) {
|
||||||
}
|
return _neoClient.addFavorite(
|
||||||
}
|
mediaId: mediaId,
|
||||||
|
mediaType: mediaType,
|
||||||
Future<List<Movie>> searchMovies(String query, {int page = 1}) async {
|
title: title,
|
||||||
final moviesUri = Uri.parse('$_baseUrl/movies/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
|
posterPath: posterPath,
|
||||||
final tvUri = Uri.parse('$_baseUrl/tv/search?query=${Uri.encodeQueryComponent(query)}&page=$page');
|
|
||||||
|
|
||||||
final responses = await Future.wait([
|
|
||||||
_client.get(moviesUri),
|
|
||||||
_client.get(tvUri),
|
|
||||||
]);
|
|
||||||
|
|
||||||
List<Movie> combined = [];
|
|
||||||
|
|
||||||
for (final response in responses) {
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final decoded = json.decode(response.body);
|
|
||||||
List<dynamic> listData;
|
|
||||||
if (decoded is List) {
|
|
||||||
listData = decoded;
|
|
||||||
} else if (decoded is Map && decoded['results'] is List) {
|
|
||||||
listData = decoded['results'];
|
|
||||||
} else {
|
|
||||||
listData = [];
|
|
||||||
}
|
|
||||||
combined.addAll(listData.map((json) => Movie.fromJson(json)));
|
|
||||||
} else {
|
|
||||||
// ignore non-200 but log maybe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combined.isEmpty) {
|
|
||||||
throw Exception('Failed to search movies/tv');
|
|
||||||
}
|
|
||||||
return combined;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Movie> _fetchMovieDetail(String path) async {
|
|
||||||
final uri = Uri.parse('$_baseUrl$path');
|
|
||||||
final response = await _client.get(uri);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final data = json.decode(response.body);
|
|
||||||
return Movie.fromJson(data);
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to load media details: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Favorites
|
|
||||||
Future<List<Favorite>> getFavorites() async {
|
|
||||||
final response = await _client.get(Uri.parse('$_baseUrl/favorites'));
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final List<dynamic> data = json.decode(response.body);
|
|
||||||
return data.map((json) => Favorite.fromJson(json)).toList();
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to fetch favorites');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addFavorite(String mediaId, String mediaType, String title, String posterPath) async {
|
|
||||||
final response = await _client.post(
|
|
||||||
Uri.parse('$_baseUrl/favorites/$mediaId?mediaType=$mediaType'),
|
|
||||||
body: json.encode({
|
|
||||||
'title': title,
|
|
||||||
'posterPath': posterPath,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 201 && response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to add favorite');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeFavorite(String mediaId) async {
|
Future<void> removeFavorite(String mediaId) {
|
||||||
final response = await _client.delete(
|
return _neoClient.removeFavorite(mediaId);
|
||||||
Uri.parse('$_baseUrl/favorites/$mediaId'),
|
}
|
||||||
|
|
||||||
|
// ---- Reactions ----
|
||||||
|
Future<Map<String, int>> getReactionCounts(
|
||||||
|
String mediaType, String mediaId) {
|
||||||
|
return _neoClient.getReactionCounts(
|
||||||
|
mediaType: mediaType,
|
||||||
|
mediaId: mediaId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to remove favorite');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactions
|
Future<void> setReaction(
|
||||||
Future<Map<String, int>> getReactionCounts(String mediaType, String mediaId) async {
|
String mediaType, String mediaId, String reactionType) {
|
||||||
final response = await _client.get(
|
return _neoClient.setReaction(
|
||||||
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/counts'),
|
mediaType: mediaType,
|
||||||
|
mediaId: mediaId,
|
||||||
|
reactionType: reactionType,
|
||||||
);
|
);
|
||||||
|
|
||||||
print('REACTION COUNTS RESPONSE (${response.statusCode}): ${response.body}');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final decoded = json.decode(response.body);
|
|
||||||
print('PARSED: $decoded');
|
|
||||||
|
|
||||||
if (decoded is Map) {
|
|
||||||
final mapSrc = decoded.containsKey('data') && decoded['data'] is Map
|
|
||||||
? decoded['data'] as Map<String, dynamic>
|
|
||||||
: decoded;
|
|
||||||
|
|
||||||
print('MAPPING: $mapSrc');
|
|
||||||
return mapSrc.map((k, v) {
|
|
||||||
int count;
|
|
||||||
if (v is num) {
|
|
||||||
count = v.toInt();
|
|
||||||
} else if (v is String) {
|
|
||||||
count = int.tryParse(v) ?? 0;
|
|
||||||
} else {
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
return MapEntry(k, count);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (decoded is List) {
|
|
||||||
// list of {type,count}
|
|
||||||
Map<String, int> res = {};
|
|
||||||
for (var item in decoded) {
|
|
||||||
if (item is Map && item['type'] != null) {
|
|
||||||
res[item['type'].toString()] = (item['count'] as num?)?.toInt() ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to fetch reactions counts');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserReaction> getMyReaction(String mediaType, String mediaId) async {
|
Future<List<UserReaction>> getMyReactions() {
|
||||||
final response = await _client.get(
|
return _neoClient.getMyReactions();
|
||||||
Uri.parse('$_baseUrl/reactions/$mediaType/$mediaId/my-reaction'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final decoded = json.decode(response.body);
|
|
||||||
if (decoded == null || (decoded is String && decoded.isEmpty)) {
|
|
||||||
return UserReaction(reactionType: null);
|
|
||||||
}
|
|
||||||
return UserReaction.fromJson(decoded as Map<String, dynamic>);
|
|
||||||
} else if (response.statusCode == 404) {
|
|
||||||
return UserReaction(reactionType: 'none'); // No reaction found
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to fetch user reaction');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setReaction(String mediaType, String mediaId, String reactionType) async {
|
// ---- Auth ----
|
||||||
final response = await _client.post(
|
Future<void> register(String name, String email, String password) {
|
||||||
Uri.parse('$_baseUrl/reactions'),
|
return _neoClient.register(
|
||||||
headers: {'Content-Type': 'application/json'},
|
name: name,
|
||||||
body: json.encode({'mediaId': '${mediaType}_${mediaId}', 'type': reactionType}),
|
email: email,
|
||||||
);
|
password: password,
|
||||||
|
).then((_) {}); // старый код ничего не возвращал
|
||||||
if (response.statusCode != 201 && response.statusCode != 200 && response.statusCode != 204) {
|
|
||||||
throw Exception('Failed to set reaction: ${response.statusCode} ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Auth Methods ---
|
Future<AuthResponse> login(String email, String password) {
|
||||||
|
return _neoClient.login(email: email, password: password);
|
||||||
Future<void> register(String name, String email, String password) async {
|
|
||||||
final uri = Uri.parse('$_baseUrl/auth/register');
|
|
||||||
final response = await _client.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode({'name': name, 'email': email, 'password': password}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
|
||||||
final decoded = json.decode(response.body) as Map<String, dynamic>;
|
|
||||||
if (decoded['success'] == true || decoded.containsKey('token')) {
|
|
||||||
// registration succeeded; nothing further to return
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to register: ${decoded['message'] ?? 'Unknown error'}');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to register: ${response.statusCode} ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthResponse> login(String email, String password) async {
|
Future<void> verify(String email, String code) {
|
||||||
final uri = Uri.parse('$_baseUrl/auth/login');
|
return _neoClient.verifyEmail(email: email, code: code).then((_) {});
|
||||||
final response = await _client.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode({'email': email, 'password': password}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return AuthResponse.fromJson(json.decode(response.body));
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to login: ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verify(String email, String code) async {
|
Future<void> resendCode(String email) {
|
||||||
final uri = Uri.parse('$_baseUrl/auth/verify');
|
return _neoClient.resendVerificationCode(email);
|
||||||
final response = await _client.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode({'email': email, 'code': code}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to verify code: ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resendCode(String email) async {
|
Future<void> deleteAccount() {
|
||||||
final uri = Uri.parse('$_baseUrl/auth/resend-code');
|
return _neoClient.deleteAccount();
|
||||||
final response = await _client.post(
|
|
||||||
uri,
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: json.encode({'email': email}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to resend code: ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteAccount() async {
|
|
||||||
final uri = Uri.parse('$_baseUrl/auth/profile');
|
|
||||||
final response = await _client.delete(uri);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to delete account: ${response.body}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Movie Methods ---
|
|
||||||
|
|
||||||
Future<List<Movie>> _fetchMovies(String endpoint, {int page = 1}) async {
|
|
||||||
final uri = Uri.parse('$_baseUrl$endpoint').replace(queryParameters: {
|
|
||||||
'page': page.toString(),
|
|
||||||
});
|
|
||||||
final response = await _client.get(uri);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final List<dynamic> data = json.decode(response.body)['results'];
|
|
||||||
if (data == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return data.map((json) => Movie.fromJson(json)).toList();
|
|
||||||
} else {
|
|
||||||
throw Exception('Failed to load movies from $endpoint');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user