diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md
new file mode 100644
index 0000000..6213f94
--- /dev/null
+++ b/BUILD_INSTRUCTIONS.md
@@ -0,0 +1,77 @@
+# Build Instructions for TorrentStreamer
+
+## Решение проблемы с libarclite
+
+Если вы получаете ошибку:
+```
+SDK does not contain 'libarclite' at the path '/Applications/Xcode-16.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a'; try increasing the minimum deployment target
+```
+
+Эта проблема возникает при использовании новых версий Xcode (16.x) со старыми CocoaPods зависимостями.
+
+## Исправления, которые были применены:
+
+### 1. Обновлен Podfile
+- Добавлен минимальный deployment target: `platform :ios, '12.0'`
+- Добавлен post_install скрипт для принудительного обновления deployment target всех подов
+- Исключена архитектура arm64 для симулятора
+
+### 2. Post-install скрипт
+```ruby
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
+ config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
+ end
+ end
+end
+```
+
+## Инструкции по сборке:
+
+1. Убедитесь, что у вас установлен CocoaPods:
+ ```bash
+ gem install cocoapods
+ ```
+
+2. Установите зависимости:
+ ```bash
+ pod install
+ ```
+
+3. Откройте проект в Xcode используя **workspace файл**:
+ ```bash
+ open grabber.xcworkspace
+ ```
+
+ ⚠️ **Важно**: Используйте `.xcworkspace`, а не `.xcodeproj`!
+
+4. Выберите целевое устройство или симулятор
+
+5. Нажмите Cmd+B для сборки или Cmd+R для запуска
+
+## Установленные зависимости:
+
+- **PopcornTorrent** (1.3.15) - для работы с торрентами
+- **MobileVLCKit** (3.3.17) - медиаплеер
+- **Kanna** (5.2.7) - HTML парсер
+- **GCDWebServer** (3.5.4) - веб-сервер (зависимость PopcornTorrent)
+
+## Требования:
+
+- iOS 12.0+
+- Xcode 12.0+
+- Swift 5.0+
+
+## Возможные проблемы:
+
+1. **Ошибка libarclite**: Решена обновлением deployment target
+2. **Проблемы с симулятором на M1 Mac**: Исключена архитектура arm64 для симулятора
+3. **Старые версии подов**: Принудительно обновлен deployment target через post_install
+
+Если у вас все еще возникают проблемы, попробуйте:
+```bash
+pod deintegrate
+pod install
+```
\ No newline at end of file
diff --git a/Podfile b/Podfile
index c001a70..63b1008 100644
--- a/Podfile
+++ b/Podfile
@@ -1,6 +1,6 @@
-# Uncomment this line to define a global platform for your project
-# platform :ios, '8.0'
-# Uncomment this line if you're using Swift
+# Define minimum iOS version to avoid libarclite issues
+platform :ios, '12.0'
+
use_frameworks!
source 'https://github.com/CocoaPods/Specs'
@@ -8,12 +8,29 @@ source 'https://github.com/PopcornTimeTV/Specs'
def pods
pod 'PopcornTorrent'
- use_frameworks!
pod 'MobileVLCKit'
end
target 'grabber' do
pods
- pod 'Kanna', :git => 'https://github.com/tid-kijyun/Kanna'
+ pod 'Kanna', '~> 5.2.0'
+end
+
+# Post install script to fix deployment target and Swift module issues
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
+ config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
+
+ # Fix Swift module compilation issues
+ if target.name == 'Kanna'
+ config.build_settings['SWIFT_VERSION'] = '5.0'
+ config.build_settings['DEFINES_MODULE'] = 'YES'
+ config.build_settings['SWIFT_INSTALL_OBJC_HEADER'] = 'YES'
+ config.build_settings['SWIFT_OBJC_INTERFACE_HEADER_NAME'] = '$(SWIFT_MODULE_NAME)-Swift.h'
+ end
+ end
+ end
end
diff --git a/Podfile.lock b/Podfile.lock
index 9207484..2554f98 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -3,37 +3,29 @@ PODS:
- GCDWebServer/Core (= 3.5.4)
- GCDWebServer/Core (3.5.4)
- Kanna (5.2.7)
- - MobileVLCKit (3.3.17)
+ - MobileVLCKit (3.6.0)
- PopcornTorrent (1.3.15):
- GCDWebServer (~> 3.5.0)
DEPENDENCIES:
- - Kanna (from `https://github.com/tid-kijyun/Kanna`)
+ - Kanna (~> 5.2.0)
- MobileVLCKit
- PopcornTorrent
SPEC REPOS:
https://github.com/CocoaPods/Specs:
- GCDWebServer
+ - Kanna
- MobileVLCKit
https://github.com/PopcornTimeTV/Specs:
- PopcornTorrent
-EXTERNAL SOURCES:
- Kanna:
- :git: https://github.com/tid-kijyun/Kanna
-
-CHECKOUT OPTIONS:
- Kanna:
- :commit: bf19cf5f5fff3924cc2b63dd32732bce28b4c748
- :git: https://github.com/tid-kijyun/Kanna
-
SPEC CHECKSUMS:
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234
- MobileVLCKit: c5dd1b0ae933dc9c2348099840e753496dcdd908
+ MobileVLCKit: 8fe98ae53b7464f32e4bdf527cc7d53053e4d3a5
PopcornTorrent: 343ff0e04ba370764a9cf27c35685463f7428b90
-PODFILE CHECKSUM: 1dbfce40a7d83a5f130b477ba9e085c90931dc2a
+PODFILE CHECKSUM: 9eca34e50e6da5d2b5d6eebd0cb94e8c6c4841ab
-COCOAPODS: 1.11.3
+COCOAPODS: 1.16.2
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..252a685
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# TorrentStreamer
+
+A native iOS application for streaming torrents directly on your device. This app allows users to search, browse, and stream torrent content using integrated torrent streaming capabilities.
+
+## Features
+
+- **Torrent Search**: Search for torrents through integrated APIs
+- **Direct Streaming**: Stream torrent content without waiting for complete downloads
+- **RuTracker Integration**: Built-in support for RuTracker.org torrent tracker
+- **VLC Player Integration**: Uses MobileVLCKit for robust media playback
+- **Core Data Storage**: Persistent storage for torrent metadata and user preferences
+
+## Requirements
+
+- iOS 8.0+
+- Xcode 10.0+
+- Swift 5.0+
+- CocoaPods
+
+## Installation
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/StasGen/TorrentStreamer.git
+ cd TorrentStreamer
+ ```
+
+2. Install dependencies using CocoaPods:
+ ```bash
+ pod install
+ ```
+
+3. Open the workspace file:
+ ```bash
+ open grabber.xcworkspace
+ ```
+
+4. Build and run the project in Xcode.
+
+## Dependencies
+
+The project uses the following key dependencies managed through CocoaPods:
+
+- **PopcornTorrent**: Core torrent streaming functionality
+- **MobileVLCKit**: Video playback capabilities
+- **Kanna**: HTML parsing for web scraping
+
+For a complete list of dependencies, see the [`Podfile`](Podfile).
+
+## Project Structure
+
+```
+grabber/
+├── AppDelegate.swift # Main application delegate
+├── Models/ # Data models
+│ ├── TorrentPreviewModel.swift
+│ └── TorrentDetailModel.swift
+├── Networking/ # Network layer
+│ ├── Core/ # Core networking components
+│ └── RutrackerApi/ # RuTracker API integration
+├── Presentation/ # View controllers and UI
+├── Helpers/ # Utility classes and extensions
+├── Extensions/ # Swift extensions
+└── Resources/ # Assets and resources
+```
+
+## Usage
+
+1. Launch the app on your iOS device or simulator
+2. Use the search functionality to find torrents
+3. Select a torrent from the search results
+4. The app will begin streaming the content using the integrated torrent client
+5. Enjoy streaming content directly without waiting for full downloads
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+4. Push to the branch (`git push origin feature/amazing-feature`)
+5. Open a Pull Request
+
+## License
+
+This project is available under the MIT License. See the LICENSE file for more details.
+
+## Disclaimer
+
+This application is for educational purposes only. Users are responsible for ensuring they comply with all applicable laws and regulations regarding torrent usage in their jurisdiction. The developers do not endorse or encourage piracy or copyright infringement.
+
+## Author
+
+Created by Станислав Калиберов (Stanislav Kaliberov)
+
+---
+
+**Note**: This application requires proper configuration and may need additional setup for full functionality. Please ensure you have the necessary permissions and legal rights to use torrent streaming services in your region.
\ No newline at end of file
diff --git a/doc/BUSINESS_PROCESS_DESCRIPTION.md b/doc/BUSINESS_PROCESS_DESCRIPTION.md
new file mode 100644
index 0000000..f2f7b2a
--- /dev/null
+++ b/doc/BUSINESS_PROCESS_DESCRIPTION.md
@@ -0,0 +1,173 @@
+# Описание бизнес-процессов TorrentStreamer
+
+## Обзор
+
+TorrentStreamer - это iOS приложение для поиска, стриминга и воспроизведения торрент-контента. Приложение интегрируется с RuTracker API для поиска торрентов и использует VLC плеер для воспроизведения видео.
+
+## Основные бизнес-процессы
+
+### 1. Процесс поиска торрентов
+
+**Участники:** Пользователь, RuTracker API, HTML Parser
+
+**Описание:** Пользователь вводит поисковый запрос, система выполняет поиск через RuTracker API и отображает результаты.
+
+**Шаги:**
+1. Пользователь вводит поисковый запрос
+2. Система отправляет запрос к RuTracker API
+3. HTML Parser обрабатывает полученные результаты
+4. Система отображает список найденных торрентов
+
+**Возможные ошибки:**
+- Ошибка подключения к API
+- Ошибка парсинга HTML
+- Отсутствие результатов поиска
+
+### 2. Процесс просмотра деталей торрента
+
+**Участники:** Пользователь, RuTracker API, HTML Parser
+
+**Описание:** Пользователь выбирает торрент из списка для просмотра подробной информации.
+
+**Шаги:**
+1. Пользователь выбирает торрент из списка результатов
+2. Система запрашивает детальную информацию через API
+3. HTML Parser извлекает детали торрента
+4. Система отображает подробную информацию
+
+**Точки принятия решений:**
+- Пользователь может выбрать воспроизведение или вернуться к поиску
+
+### 3. Процесс стриминга торрента
+
+**Участники:** Пользователь, Torrent Streamer, File System
+
+**Описание:** Система скачивает торрент-файл и начинает стриминг контента.
+
+**Шаги:**
+1. Система скачивает .torrent файл
+2. Запускается торрент-стриминг
+3. Проверка количества файлов в торренте
+4. Если несколько файлов - пользователь выбирает нужный
+5. Подготовка видео потока для воспроизведения
+
+**Возможные ошибки:**
+- Ошибка скачивания торрент-файла
+- Ошибка запуска стриминга
+- Недоступность файлов
+
+### 4. Процесс воспроизведения видео
+
+**Участники:** Пользователь, VLC Media Player
+
+**Описание:** Воспроизведение видео контента через встроенный VLC плеер.
+
+**Шаги:**
+1. Система передает видео поток в VLC плеер
+2. Пользователь просматривает видео
+3. По завершению пользователь может:
+ - Вернуться к деталям торрента
+ - Начать новый поиск
+ - Выйти из приложения
+
+**Управление:**
+- Долгое нажатие (2 сек) для выхода из плеера
+
+### 5. Процесс обработки ошибок
+
+**Участники:** Пользователь, Error Handler
+
+**Описание:** Система обрабатывает возникающие ошибки и предоставляет пользователю варианты действий.
+
+**Типы ошибок:**
+- Ошибки API (сетевые проблемы, недоступность сервера)
+- Ошибки стриминга (проблемы с торрент-файлами)
+
+**Варианты действий:**
+- Повторить операцию
+- Выйти из приложения
+
+## Архитектурные особенности
+
+### Ветка main (MVC)
+- Традиционная MVC архитектура
+- Вся бизнес-логика в контроллерах
+- Прямые вызовы API
+
+### Ветка detailVC-mvvm (MVVM + Combine)
+- MVVM архитектура с использованием Combine
+- Реактивное программирование
+- Четкое разделение ответственности
+- Автоматическое обновление UI
+
+## Ключевые компоненты системы
+
+### 1. RutrackerApiManager
+- Управление запросами к RuTracker API
+- Аутентификация пользователей
+- Обработка поисковых запросов
+
+### 2. HTML Parser
+- Извлечение данных из HTML страниц RuTracker
+- Парсинг списков торрентов
+- Извлечение деталей торрентов
+
+### 3. Torrent Streamer
+- Управление торрент-загрузками
+- Стриминг контента
+- Управление файловой системой
+
+### 4. VLC Media Player
+- Воспроизведение видео контента
+- Поддержка различных форматов
+- Управление воспроизведением
+
+## Состояния системы
+
+### Состояния экрана деталей торрента (MVVM)
+```swift
+enum State {
+ case initial
+ case loading
+ case loaded(TorrentDetailModel)
+ case streaming(TorrentStreamerModel)
+ case error(Error)
+}
+```
+
+### Параметры поиска
+- Тип сортировки (по количеству сидов, дате создания и т.д.)
+- Период создания (последний месяц, год и т.д.)
+- Тип торрента (новинки, сериалы и т.д.)
+
+## Интеграции
+
+### Внешние сервисы
+- **RuTracker.org** - источник торрент-контента
+- **VLC Media Player** - воспроизведение видео
+
+### Внутренние компоненты
+- **UIKit** - пользовательский интерфейс
+- **Combine** - реактивное программирование (в MVVM ветке)
+- **Kanna** - парсинг HTML
+- **MobileVLCKit** - интеграция с VLC
+
+## Безопасность и ограничения
+
+### Аутентификация
+- Жестко закодированные учетные данные для RuTracker
+- Необходимо вынести в конфигурацию
+
+### Ограничения
+- Зависимость от доступности RuTracker.org
+- Требуется подключение к интернету
+- Ограничения iOS на фоновые загрузки
+
+## Возможности для улучшения
+
+1. **Кэширование** - сохранение результатов поиска
+2. **Офлайн режим** - просмотр ранее загруженного контента
+3. **Пользовательские настройки** - персонализация интерфейса
+4. **Расширенный поиск** - дополнительные фильтры
+5. **Социальные функции** - рейтинги, комментарии
+6. **Безопасность** - шифрование, VPN интеграция
\ No newline at end of file
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..652743c
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,91 @@
+# 📚 Документация TorrentStreamer
+
+Добро пожаловать в документацию проекта TorrentStreamer - iOS приложения для поиска, стриминга и воспроизведения торрент-контента.
+
+## 📋 Содержание документации
+
+### 🔄 [Бизнес-процессы](./BUSINESS_PROCESS_DESCRIPTION.md)
+Подробное описание всех бизнес-процессов приложения, включая:
+- Процесс поиска торрентов
+- Просмотр деталей торрента
+- Стриминг контента
+- Воспроизведение видео
+- Обработка ошибок
+
+### 📊 BPMN Диаграммы
+
+#### [Визуальная диаграмма](./torrent_streamer_bpmn_diagram.png)
+
+
+Наглядное представление всех бизнес-процессов с цветовой кодировкой:
+- 🟢 События (Начало/Конец)
+- 🔵 Пользовательские задачи
+- 🟣 Сервисные задачи
+- 🟡 Шлюзы (Точки принятия решений)
+- 🔴 События ошибок
+
+#### [BPMN XML файл](./torrent_streamer_business_process.bpmn)
+Стандартный BPMN 2.0 файл, который можно открыть в любом BPMN редакторе:
+- Camunda Modeler
+- Bizagi Modeler
+- Draw.io
+- Lucidchart
+
+## 🏗️ Архитектура проекта
+
+### Ветки проекта
+- **main** - MVC архитектура
+- **detailVC-mvvm** - MVVM архитектура с Combine framework
+
+### Ключевые компоненты
+1. **RutrackerApiManager** - управление API запросами
+2. **HTML Parser** - парсинг данных с RuTracker
+3. **Torrent Streamer** - управление торрент-загрузками
+4. **VLC Media Player** - воспроизведение видео
+
+## 🚀 Основные функции
+
+### Поиск торрентов
+- Интеграция с RuTracker.org API
+- Фильтрация по категориям и параметрам
+- Парсинг HTML результатов
+
+### Стриминг
+- Загрузка торрент-файлов
+- Потоковое воспроизведение
+- Поддержка множественных файлов
+
+### Воспроизведение
+- Интеграция с VLC плеером
+- Поддержка различных видео форматов
+- Управление воспроизведением
+
+## 🔧 Технологии
+
+- **iOS SDK** - нативная разработка
+- **UIKit** - пользовательский интерфейс
+- **Combine** - реактивное программирование (MVVM ветка)
+- **Kanna** - HTML парсинг
+- **MobileVLCKit** - видео плеер
+- **Swift** - язык программирования
+
+## 📈 Возможности для развития
+
+1. **Кэширование результатов поиска**
+2. **Офлайн режим просмотра**
+3. **Пользовательские настройки**
+4. **Расширенные фильтры поиска**
+5. **Социальные функции**
+6. **Улучшения безопасности**
+
+## 🤝 Вклад в проект
+
+При внесении изменений в проект, пожалуйста:
+1. Обновляйте соответствующую документацию
+2. Следуйте архитектурным принципам выбранной ветки
+3. Добавляйте тесты для новой функциональности
+4. Обновляйте BPMN диаграммы при изменении бизнес-процессов
+
+---
+
+*Документация создана автоматически на основе анализа кода проекта*
\ No newline at end of file
diff --git a/doc/torrent_streamer_bpmn_diagram.png b/doc/torrent_streamer_bpmn_diagram.png
new file mode 100644
index 0000000..c740596
Binary files /dev/null and b/doc/torrent_streamer_bpmn_diagram.png differ
diff --git a/doc/torrent_streamer_business_process.bpmn b/doc/torrent_streamer_business_process.bpmn
new file mode 100644
index 0000000..ef9a0d8
--- /dev/null
+++ b/doc/torrent_streamer_business_process.bpmn
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+ Flow_1
+
+
+
+
+ Flow_1
+ Flow_BackToSearch
+ Flow_2
+
+
+
+
+ Flow_2
+ Flow_3
+
+
+
+
+ Flow_3
+ Flow_4
+
+
+
+
+ Flow_4
+ Flow_5
+
+
+
+
+ Flow_5
+ Flow_6
+
+
+
+
+ Flow_6
+ Flow_ReturnToDetails
+ Flow_7_Play
+ Flow_8_Back
+
+
+
+
+ Flow_7_Play
+ Flow_9
+
+
+
+
+ Flow_9
+ Flow_FileSelected
+ Flow_10
+
+
+
+
+ Flow_10
+ Flow_11_Single
+ Flow_12_Multiple
+
+
+
+
+ Flow_12_Multiple
+ Flow_FileSelected
+
+
+
+
+ Flow_11_Single
+ Flow_13
+
+
+
+
+ Flow_13
+ Flow_14
+
+
+
+
+ Flow_14
+ Flow_ReturnToDetails
+ Flow_BackToSearch
+ Flow_15_Exit
+
+
+
+
+ Flow_15_Exit
+ Flow_8_Back
+
+
+
+
+ Flow_Error1
+
+
+
+
+ Flow_Error2
+
+
+
+
+ Flow_Error1
+ Flow_Error2
+ Flow_RetryOrExit
+
+
+
+ Flow_RetryOrExit
+ Flow_Retry
+ Flow_ExitError
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/grabber.xcodeproj/project.pbxproj b/grabber.xcodeproj/project.pbxproj
index 206c094..6e19234 100644
--- a/grabber.xcodeproj/project.pbxproj
+++ b/grabber.xcodeproj/project.pbxproj
@@ -25,6 +25,7 @@
E6A06E4F28FC587700BF59D8 /* RutrackerSearchParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A06E4E28FC587700BF59D8 /* RutrackerSearchParameters.swift */; };
E6A06E5128FC7B3C00BF59D8 /* ApiServiceToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A06E5028FC7B3C00BF59D8 /* ApiServiceToken.swift */; };
E6A06E5328FC7B5000BF59D8 /* ApiServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A06E5228FC7B5000BF59D8 /* ApiServiceError.swift */; };
+ E6DC683829066FD80042C446 /* TorrentDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DC683729066FD80042C446 /* TorrentDetailViewModel.swift */; };
FBEF943A7D3312F16C8AE13F /* Pods_grabber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F7388E8B295BC6228EDE231 /* Pods_grabber.framework */; };
/* End PBXBuildFile section */
@@ -53,6 +54,7 @@
E6A06E4E28FC587700BF59D8 /* RutrackerSearchParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RutrackerSearchParameters.swift; sourceTree = ""; };
E6A06E5028FC7B3C00BF59D8 /* ApiServiceToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceToken.swift; sourceTree = ""; };
E6A06E5228FC7B5000BF59D8 /* ApiServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceError.swift; sourceTree = ""; };
+ E6DC683729066FD80042C446 /* TorrentDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorrentDetailViewModel.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -156,9 +158,9 @@
E6A06E4428FB4D2300BF59D8 /* Presentation */ = {
isa = PBXGroup;
children = (
+ E6DC683629066FC60042C446 /* TorrentDetail */,
E6A06E4628FB4DC100BF59D8 /* VLCPlayerViewConttroller */,
855A6B692027A8710014E28B /* TorrentSearchListViewController.swift */,
- 855A6B7E202F96DA0014E28B /* TorrentDetailViewController.swift */,
855A6B732027A8710014E28B /* LaunchScreen.storyboard */,
855A6B6B2027A8710014E28B /* Main.storyboard */,
);
@@ -201,6 +203,15 @@
path = RutrackerApi;
sourceTree = "";
};
+ E6DC683629066FC60042C446 /* TorrentDetail */ = {
+ isa = PBXGroup;
+ children = (
+ 855A6B7E202F96DA0014E28B /* TorrentDetailViewController.swift */,
+ E6DC683729066FD80042C446 /* TorrentDetailViewModel.swift */,
+ );
+ path = TorrentDetail;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -329,6 +340,7 @@
B8935CBC28F8913A00FCD27C /* RutrackerApiManager.swift in Sources */,
855A6B6A2027A8710014E28B /* TorrentSearchListViewController.swift in Sources */,
B84BCF7B28F8B02C00353BA2 /* TorrentDetailModel.swift in Sources */,
+ E6DC683829066FD80042C446 /* TorrentDetailViewModel.swift in Sources */,
855A6B682027A8710014E28B /* AppDelegate.swift in Sources */,
E6A06E5328FC7B5000BF59D8 /* ApiServiceError.swift in Sources */,
B8D8850328F86F7000E704D8 /* Rutracker_org_HtmlParser.swift in Sources */,
diff --git a/grabber/Presentation/TorrentDetail/TorrentDetailViewController.swift b/grabber/Presentation/TorrentDetail/TorrentDetailViewController.swift
new file mode 100644
index 0000000..adb642c
--- /dev/null
+++ b/grabber/Presentation/TorrentDetail/TorrentDetailViewController.swift
@@ -0,0 +1,110 @@
+//
+// TorrentDetailViewController.swift
+// grabber
+//
+// Created by Станислав Калиберов on 10.02.2018.
+// Copyright © 2018 Станислав Калиберов. All rights reserved.
+//
+
+import UIKit
+import Combine
+
+class TorrentDetailViewController: UIViewController {
+ @IBOutlet weak var bobodyTextViewdyLabel: UITextView!
+ @IBOutlet weak var backgroundImageView: UIImageView!
+ @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
+ @IBOutlet weak var videoControlButton: UIButton!
+
+ var viewModel: TorrentDetailViewModel!
+ private var cancellable: Set = []
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ viewModel.$state.sink { [weak self] s in
+ DispatchQueue.main.async {
+ self?.render(state: s)
+ }
+ }.store(in: &cancellable)
+ viewModel.viewDidLoad()
+ }
+
+ private func render(state: TorrentDetailViewModel.State) {
+ switch state {
+ case .initial:
+ // initial setup ui
+ break
+
+ case .fetchTrackerInfo:
+ activityIndicatorView.startAnimating()
+ activityIndicatorView.isHidden = false
+
+ case .readyForStream(let info):
+ info.image.flatMap { ImageViewDownloader().perform(from: $0, imageView: self.backgroundImageView)}
+ self.title = info.name
+ self.bobodyTextViewdyLabel.text = info.body
+ activityIndicatorView.isHidden = true
+
+ case .downloadTorrentFile:
+ activityIndicatorView.isHidden = false
+
+ case .startStream:
+ activityIndicatorView.isHidden = false
+
+ case .needToSelectFileIndex(let files):
+ activityIndicatorView.isHidden = true
+ presentAlert(
+ title: "Select file to play",
+ items: files
+ .enumerated()
+ .map { i in (i.element, { _ in self.viewModel.selectTorrentFile(index: i.offset) }) }
+ .filter { !$0.0.hasSuffix(".srt") },
+ cancel: { [weak self] _ in
+ self?.viewModel.returnToReadyForStream()
+ }
+ )
+ case .streamingAndShowPlayer(videoFileURL: let videoFileURL):
+ activityIndicatorView.isHidden = true
+ performSegue(withIdentifier: C.showVLCPlayerSegueId, sender: videoFileURL)
+
+ case .failed(let oldState, let error):
+ activityIndicatorView.isHidden = true
+ presentAlert(title: "Error", message: "\(error), for state \(oldState)")
+ }
+ }
+
+ private func presentAlert(
+ title: String,
+ message: String? = nil,
+ items: [(String, (UIAlertAction)->())] = [],
+ cancel: ((UIAlertAction)->())? = nil
+ ) {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ items
+ .map { UIAlertAction(title: $0.0, style: .default, handler: $0.1)}
+ .forEach(alert.addAction)
+ if let cancel = cancel {
+ alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: cancel))
+ }
+ present(alert, animated: true, completion: nil)
+ }
+
+ @IBAction func didTapVideoControlButton(_ sender: UIButton) {
+ viewModel.playButtonDidTap()
+ }
+
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+ if segue.identifier == C.showVLCPlayerSegueId, let controller = segue.destination as? VLCPlayerViewConttroller {
+ controller.videoFileURL = sender as? URL
+ controller.onDismiss = { [weak self] in
+ self?.viewModel.returnToReadyForStream()
+ }
+ }
+ }
+}
+
+extension TorrentDetailViewController {
+ enum C {
+ static let showVLCPlayerSegueId = "ShowVLCPlayerViewController"
+ }
+}
diff --git a/grabber/Presentation/TorrentDetail/TorrentDetailViewModel.swift b/grabber/Presentation/TorrentDetail/TorrentDetailViewModel.swift
new file mode 100644
index 0000000..f12da03
--- /dev/null
+++ b/grabber/Presentation/TorrentDetail/TorrentDetailViewModel.swift
@@ -0,0 +1,178 @@
+//
+// TorrentDetailViewModel.swift
+// grabber
+//
+// Created by Stanislav Kaliberov on 24.10.2022.
+// Copyright © 2022 Станислав Калиберов. All rights reserved.
+//
+
+import Foundation
+import PopcornTorrent
+import MobileVLCKit
+import Combine
+
+class TorrentDetailViewModel {
+ indirect enum State {
+ case initial
+ case fetchTrackerInfo
+ case readyForStream(TorrentDetailModel)
+ case downloadTorrentFile
+ case startStream(URL)
+ case needToSelectFileIndex([String])
+ case streamingAndShowPlayer(videoFileURL: URL)
+ case failed(State, Error)
+ }
+
+ init(
+ id: String,
+ torrentFileUrl: URL? = nil,
+ info: TorrentDetailModel? = nil,
+ fileIndexForPlay: Int? = nil
+ ) {
+ self.id = id
+ self.torrentFileUrl = torrentFileUrl
+ self.info = info
+ self.fileIndexForPlay = fileIndexForPlay
+ }
+
+ private var id: String
+ private var torrentFileUrl: URL?
+ private var info: TorrentDetailModel?
+ private var fileIndexForPlay: Int?
+
+ @Published var state: State = .initial {
+ didSet {
+ switch state {
+ case .initial:
+ break
+
+ case .fetchTrackerInfo:
+ fetchTrackerInfo()
+
+ case .readyForStream(let detailModel):
+ info = detailModel
+ streamer.cancelStreamingAndDeleteData(true)
+
+ case .downloadTorrentFile:
+ fetchFile()
+
+ case .startStream(let url):
+ torrentFileUrl = url
+ play(fileUrl: url)
+
+ case .needToSelectFileIndex:
+ break
+
+ case .streamingAndShowPlayer:
+ break
+
+ case .failed:
+ break
+ }
+ }
+ }
+
+ private let api: RutrackerApiManager = RutrackerApiManager()
+ private let streamer: PTTorrentStreamer = PTTorrentStreamer.shared()
+
+ deinit {
+ streamer.cancelStreamingAndDeleteData(true)
+ }
+
+ // MARK: - Interface
+ func viewDidLoad() {
+ print("Open tracker - ", id)
+ state = .fetchTrackerInfo
+ }
+
+ func playButtonDidTap() {
+ if case .readyForStream = state {
+ state = .downloadTorrentFile
+ } else if case .failed = state {
+ state = .fetchTrackerInfo
+ }
+ }
+
+ func selectTorrentFile(index: Int) {
+ fileIndexForPlay = index
+ if let torrentFileUrl = self.torrentFileUrl {
+ self.state = .startStream(torrentFileUrl)
+ } else {
+ self.state = .downloadTorrentFile
+ }
+ }
+
+ func returnToReadyForStream() {
+ guard let info = info else {
+ state = .fetchTrackerInfo
+ return
+ }
+ state = .readyForStream(info)
+ }
+
+
+ // MARK: - Privates
+ private func fetchTrackerInfo() {
+ api.getTrackerInfo(id: id) { [weak self] result in
+ guard let self = self else { return }
+ do {
+ self.state = .readyForStream(try result.get())
+ } catch {
+ self.state = .failed(self.state, error)
+ }
+ }
+ }
+
+ private func fetchFile() {
+ api.getTorrentFile(topicId: id) { [weak self] result in
+ guard let self = self else { return }
+ do {
+ let fileManager = FileManager.default
+ let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+ let fileURL = documentDirectory.appendingPathComponent(self.id)
+ let torrentFile = try result.get()
+ try torrentFile.write(to: fileURL)
+ self.state = .startStream(fileURL)
+
+ } catch {
+ self.state = .failed(self.state, error)
+ }
+ }
+ }
+
+ private func play(fileUrl: URL) {
+ streamer.startStreaming(
+ fromMultiTorrentFileOrMagnetLink: fileUrl.path,
+ progress: { (status) in
+ print(status)
+ },
+ readyToPlay: { [weak self] (videoFileURL, videoFilePath) in
+ if case .needToSelectFileIndex = self?.state {
+ self?.streamer.cancelStreamingAndDeleteData(true)
+ } else {
+ self?.state = .streamingAndShowPlayer(videoFileURL: videoFileURL)
+ }
+ },
+ failure: { [weak self] error in
+ guard let self = self else { return }
+ self.state = .failed(self.state, error)
+
+ }
+ ) { [weak self] result in
+ guard let self = self else { return 0 }
+ print(result)
+
+ if result.count == 1 {
+ self.fileIndexForPlay = 0
+ return 0
+
+ } else if let fileIndex = self.fileIndexForPlay {
+ return Int32(fileIndex)
+
+ } else {
+ self.state = .needToSelectFileIndex(result)
+ return 0
+ }
+ }
+ }
+}
diff --git a/grabber/Presentation/TorrentDetailViewController.swift b/grabber/Presentation/TorrentDetailViewController.swift
deleted file mode 100644
index f03a3aa..0000000
--- a/grabber/Presentation/TorrentDetailViewController.swift
+++ /dev/null
@@ -1,212 +0,0 @@
-//
-// TorrentDetailViewController.swift
-// grabber
-//
-// Created by Станислав Калиберов on 10.02.2018.
-// Copyright © 2018 Станислав Калиберов. All rights reserved.
-//
-
-import UIKit
-import PopcornTorrent
-import MobileVLCKit
-
-class TorrentDetailViewController: UIViewController {
- indirect enum State {
- case initial
- case fetchTrackerInfo
- case readyForStream
- case downloadTorrentFile
- case startStream(URL)
- case needToSelectFileIndex([String])
- case streamingAndShowPlayer(videoFileURL: URL)
- case failed(State, Error)
- }
-
- @IBOutlet weak var bobodyTextViewdyLabel: UITextView!
- @IBOutlet weak var backgroundImageView: UIImageView!
- @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
- @IBOutlet weak var videoControlButton: UIButton!
-
- var id: String!
- var torrentFileUrl: URL?
- var info: TorrentDetailModel?
- var fileIndexForPlay: Int?
-
- private let api: RutrackerApiManager = RutrackerApiManager()
- private let streamer: PTTorrentStreamer = PTTorrentStreamer.shared()
- private var state: State = .initial {
- didSet {
- print("current state = ", state)
- DispatchQueue.main.async {
- switch self.state {
- case .initial: break
-
- case .fetchTrackerInfo:
- self.activityIndicatorView.startAnimating()
- self.activityIndicatorView.isHidden = false
- self.fetchTrackerInfo()
-
- case .readyForStream:
- self.streamer.cancelStreamingAndDeleteData(true)
- self.updateUI()
- self.activityIndicatorView.isHidden = true
-
- case .downloadTorrentFile:
- self.activityIndicatorView.isHidden = false
- self.fetchFile()
-
- case .startStream(let url):
- self.activityIndicatorView.isHidden = false
- self.play(fileUrl: url)
-
- case .needToSelectFileIndex(let files):
- self.activityIndicatorView.isHidden = true
- self.presentAlert(
- title: "Select file to play",
- items: files.enumerated().map { i in
- (i.element, { _ in
- self.fileIndexForPlay = i.offset
- if let torrentFileUrl = self.torrentFileUrl {
- self.state = .startStream(torrentFileUrl)
- } else {
- self.state = .downloadTorrentFile
- }
- })
- }.filter { !$0.0.hasSuffix(".srt") },
- cancel: { _ in
- self.state = .readyForStream
- }
- )
-
- case .streamingAndShowPlayer(videoFileURL: let videoFileURL):
- self.activityIndicatorView.isHidden = true
- self.performSegue(withIdentifier: "ShowVLCPlayerViewController", sender: videoFileURL)
-
- case .failed(let oldState, let error):
- self.activityIndicatorView.isHidden = true
- self.presentAlert(title: "Error", message: "\(error), for state \(oldState)", items: [])
- }
- }
- }
- }
-
- deinit {
- streamer.cancelStreamingAndDeleteData(true)
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- print("Open tracker - ", id!)
- state = .fetchTrackerInfo
- }
-
- private func updateUI() {
- info?.image.flatMap { ImageViewDownloader().perform(from: $0, imageView: backgroundImageView)}
- title = info?.name ?? "None"
- bobodyTextViewdyLabel.text = info?.body ?? "None"
- }
-
- private func fetchTrackerInfo() {
- api.getTrackerInfo(id: id) { [weak self] result in
- guard let self = self else { return }
- do {
- self.info = try result.get()
- self.state = .readyForStream
- } catch {
- self.state = .failed(self.state, error)
- }
- }
- }
-
- private func fetchFile() {
- api.getTorrentFile(topicId: id) { [weak self] result in
- guard let self = self else { return }
- do {
- let fileManager = FileManager.default
- let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
- let fileURL = documentDirectory.appendingPathComponent(self.id)
- let torrentFile = try result.get()
- try torrentFile.write(to: fileURL)
- self.torrentFileUrl = fileURL
- self.state = .startStream(fileURL)
-
- } catch {
- self.state = .failed(self.state, error)
- }
- }
- }
-
- private func play(fileUrl: URL) {
- streamer.startStreaming(
- fromMultiTorrentFileOrMagnetLink: fileUrl.path,
- progress: { (status) in
- print(status)
- },
- readyToPlay: { [weak self] (videoFileURL, videoFilePath) in
- if case .needToSelectFileIndex = self?.state {
- self?.streamer.cancelStreamingAndDeleteData(true)
- } else {
- self?.state = .streamingAndShowPlayer(videoFileURL: videoFileURL)
- }
- },
- failure: { [weak self] error in
- guard let self = self else { return }
- self.state = .failed(self.state, error)
-
- }
- ) { [weak self] result in
- guard let self = self else { return 0 }
- print(result)
-
- if result.count == 1 {
- self.fileIndexForPlay = 0
- return 0
-
- } else if let fileIndex = self.fileIndexForPlay {
- return Int32(fileIndex)
-
- } else {
- self.state = .needToSelectFileIndex(result)
- return 0
- }
- }
- }
-
- private func presentAlert(
- title: String,
- message: String? = nil,
- items: [(String, (UIAlertAction)->())],
- cancel: ((UIAlertAction)->())? = nil
- ) {
- let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
- items
- .map { UIAlertAction(title: $0.0, style: .default, handler: $0.1)}
- .forEach(alert.addAction)
- if let cancel = cancel {
- alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: cancel))
- }
- present(alert, animated: true, completion: nil)
- }
-
- @IBAction func didTapVideoControlButton(_ sender: UIButton) {
- if case .readyForStream = state {
- state = .downloadTorrentFile
- } else if case .failed = state {
- state = .fetchTrackerInfo
- }
- }
-
- override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
- if segue.identifier == "ShowVLCPlayerViewController", let controller = segue.destination as? VLCPlayerViewConttroller {
- controller.videoFileURL = sender as? URL
- controller.onDismiss = { [weak self] in
- self?.state = .readyForStream
- }
- }
- }
-}
-
-
-
-
-
diff --git a/grabber/Presentation/TorrentSearchListViewController.swift b/grabber/Presentation/TorrentSearchListViewController.swift
index 9fdab49..36fcd7c 100644
--- a/grabber/Presentation/TorrentSearchListViewController.swift
+++ b/grabber/Presentation/TorrentSearchListViewController.swift
@@ -53,8 +53,13 @@ class TorrentSearchListViewController: UIViewController {
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
- if segue.identifier == "ShowConcertInfoSegue", let controller = segue.destination as? TorrentDetailViewController {
- controller.id = sender as? String
+ if
+ segue.identifier == "ShowConcertInfoSegue",
+ let controller = segue.destination as? TorrentDetailViewController,
+ let torrentId = sender as? String
+ {
+ let viewModel = TorrentDetailViewModel(id: torrentId)
+ controller.viewModel = viewModel
}
}