diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt index 0a7e3e1b..c0d37661 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -67,6 +67,8 @@ class TorrentService : Service() { // 3. 초기 세션 상태 적용 updateSessionState() + + startLightweightUpdater() } private fun checkInitialStatus() { @@ -123,28 +125,96 @@ class TorrentService : Service() { * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 */ private fun updateSessionState() { - if (isCharging) { - if (session.isPaused) session.resume() + if (session.isPaused) session.resume() - // Wi-Fi면 5개, 셀룰러면 1개 다운로드 허용 - val sp = SettingsPack() - if (isWifiConnected) { - sp.activeDownloads(5) - sp.activeLimit(8) - } else { - sp.activeDownloads(1) - sp.activeLimit(2) + serviceScope.launch { + val vector = session.swig().get_torrents() + // (핸들, 계산된 우선순위 점수) + val torrentsWithMetadata = mutableListOf>() + val torrentsWithoutMetadata = mutableListOf() + + val LOW_SPEED_THRESHOLD = 10 * 1024 // 10 KB/s 미만을 '속도 저조'로 판단 + val PENALTY_SCORE = 1000 // 속도가 낮을 경우 부여할 페널티 (대기열 순서보다 큰 값) + + for (i in 0 until vector.size.toInt()) { + val handle = TorrentHandle(vector.get(i)) + if (!handle.isValid) continue + + val status = handle.status() + if (!status.hasMetadata()) { + torrentsWithoutMetadata.add(handle) + } else if (!status.isFinished) { + val currentRate = status.downloadPayloadRate() + val basePriority = status.queuePosition() + + // 속도가 너무 낮거나 0인 경우 페널티 부여 + // 단, 막 시작해서 속도가 측정되지 않은 경우를 위해 추가 로직을 넣을 수 있지만 + // 주기적 업데이트(2초)가 되므로 페널티를 받아도 다음 턴에 기회를 잡을 수 있습니다. + val finalScore = if (currentRate < LOW_SPEED_THRESHOLD) { + basePriority + PENALTY_SCORE + } else { + basePriority + } + + torrentsWithMetadata.add(handle to finalScore) + } } - session.applySettings(sp) - println("TorrentService: 충전 중 - 세션 활성화 (Wi-Fi: $isWifiConnected)") - } else { - if (!session.isPaused) session.pause() - // 💡 충전 중단 시 타이머 즉시 종료 (배터리 소모 0) - updateJob?.cancel() - println("TorrentService: 충전 중 아님 - 세션 및 업데이트 타이머 일시정지") + // 1. 메타데이터 미수신: 무조건 유지 + torrentsWithoutMetadata.forEach { it.swig().resume() } + + // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 + if (isCharging) { + val maxSlots = if (isWifiConnected) 7 else 2 + val sortedByPriority = torrentsWithMetadata.sortedBy { it.second } + + sortedByPriority.forEachIndexed { index, pair -> + val handle = pair.first + if (index < maxSlots) { + handle.swig().resume() + } else { + handle.pause() + } + } + } else { + // 배터리 모드 + torrentsWithMetadata.forEach { it.first.pause() } + } + + refreshTorrentStats() + } + } + + private val TRACKER_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" + + /** + * 전 세계 최신 트래커 리스트를 가져와 현재 모든 토렌트에 주입합니다. + */ + private fun updateTrackers() { + serviceScope.launch(Dispatchers.IO) { + try { + val trackers = java.net.URL(TRACKER_URL).readText() + .split("\n") + .filter { it.isNotBlank() } + + val vector = session.swig().get_torrents() + for (i in 0 until vector.size.toInt()) { + val handle = TorrentHandle(vector.get(i)) + if (!handle.isValid) continue + + // 각 트래커를 토렌트에 추가 + trackers.forEach { trackerUrl -> + handle.addTracker(AnnounceEntry(trackerUrl)) + } + + // 트래커 추가 후 즉시 재요청(Force Reannounce) + handle.forceReannounce() + } + println("TorrentService: ${trackers.size}개의 트래커 주입 완료") + } catch (e: Exception) { + e.printStackTrace() + } } - refreshTorrentStats() // 상태 변경 후 즉시 UI 갱신 } private fun initLibTorrent() { @@ -156,6 +226,18 @@ class TorrentService : Service() { sp.setInteger(settings_pack.int_types.out_enc_policy.swigValue(), settings_pack.enc_policy.pe_enabled.swigValue()) sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true) sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true) + sp.setBoolean(settings_pack.bool_types.enable_upnp.swigValue(), true) // 공유기 포트포워딩 자동 설정 + sp.setBoolean(settings_pack.bool_types.enable_natpmp.swigValue(), true) + sp.setInteger(settings_pack.int_types.connections_limit.swigValue(), 1000) + + sp.setBoolean(settings_pack.bool_types.enable_incoming_utp.swigValue(), true) + sp.setBoolean(settings_pack.bool_types.enable_outgoing_utp.swigValue(), true) + + sp.setInteger(settings_pack.int_types.dht_announce_interval.swigValue(), 300) + + sp.connectionsLimit(1000) + sp.activeDownloads(10) + sp.activeLimit(10) session.applySettings(sp) @@ -175,7 +257,9 @@ class TorrentService : Service() { moveToPrivateStorage((alert as TorrentFinishedAlert).handle()) refreshTorrentStats() } - AlertType.METADATA_RECEIVED -> (alert as MetadataReceivedAlert).handle().saveResumeData() + AlertType.METADATA_RECEIVED -> { + (alert as MetadataReceivedAlert).handle().saveResumeData() + } AlertType.SAVE_RESUME_DATA -> { val ra = alert as SaveResumeDataAlert saveResumeFile(ra.handle().infoHash().toString(), ra.params()) @@ -188,6 +272,7 @@ class TorrentService : Service() { session.start() restoreExistingDownloads() + updateTrackers() } private var updateJob: Job? = null @@ -197,11 +282,16 @@ class TorrentService : Service() { updateJob?.cancel() updateJob = serviceScope.launch { while (isActive) { + updateSessionState() // 충전 중이고 세션이 돌아갈 때만 C++ 엔진에 상태 업데이트 요청 - if (isCharging && !session.isPaused) { + var delayTime = 2000L + if (isCharging) { session.postTorrentUpdates() + + } else { + delayTime = 10000L } - delay(2000) // 2초 주기 (원하는 대로 조절 가능) + delay(delayTime) } } } @@ -249,10 +339,11 @@ class TorrentService : Service() { val isDownloading = (state.swig() == torrent_status.state_t.downloading.swigValue()) val stateText = when { - !isCharging -> "충전 대기 중" - isPaused -> "일시정지" - isDownloading -> "다운로드 중" isFinished -> "완료" + !status.hasMetadata() -> "메타데이터 수신 중..." + !isCharging -> "충전 대기 중" + isDownloading -> "다운로드 중" + isPaused -> "일시정지" else -> "대기 중" } @@ -272,21 +363,55 @@ class TorrentService : Service() { } private fun updateNotification(tasks: List) { - if (tasks.isEmpty()) return - val activeTask = tasks.firstOrNull { !it.isPaused && it.progress < 100f } ?: tasks.first() - val currentProgress = activeTask.progress.toInt() + if (tasks.isEmpty()) { + notificationManager.cancel(NOTIFICATION_ID) + return + } - // 최적화: 진행률이 변했을 때만 notify 호출 - if (currentProgress == lastProgress) return - lastProgress = currentProgress + // 1. 메타데이터 수신 중인 파일 수 + val metadataCount = tasks.count { it.stateText == "메타데이터 수신 중..." } - notificationBuilder.setContentTitle(if (tasks.size > 1) "${activeTask.name} 외 ${tasks.size - 1}건" else activeTask.name) - .setContentText("${activeTask.stateText}: $currentProgress%") - .setProgress(100, currentProgress, false) + // 2. 실제 다운로드 중인 파일들 (충전/와이파이 조건으로 활성화된 것들) + val downloadingTasks = tasks.filter { it.isPaused == false }.filter { it.stateText == "다운로드 중" } + val downloadingCount = downloadingTasks.size + + // 3. 다운로드 중인 파일들의 평균 진행률 + val averageProgress = if (downloadingTasks.isNotEmpty()) { + downloadingTasks.map { it.progress }.average().toInt() + } else { + 0 + } + + // 4. 전체 다운로드/업로드 속도 (Session에서 직접 가져옴) + val stats = session.stats() + val downloadSpeed = formatSpeed(stats.downloadRate()) + val uploadSpeed = formatSpeed(stats.uploadRate()) + + // 알림 메시지 구성 + val title = if (downloadingCount > 0) "총 ${tasks.size} | 다운 ${downloadingCount} | 메타 ${metadataCount} | 대기 ${tasks.size - (downloadingCount + metadataCount)}" else "대기 중" + val content = StringBuilder().apply { + append("평균 다운 진행율 $averageProgress% | ") + append("↓ $downloadSpeed ↑ $uploadSpeed") + }.toString() + + notificationBuilder + .setContentTitle(title) + .setContentText(content) + .setProgress(100, averageProgress, downloadingCount == 0 && metadataCount > 0) // 메타 수신 중엔 불확정 게이지 notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) } + // 속도 포맷팅 유틸리티 + private fun formatSpeed(bytesPerSecond: Long): String { + val kb = bytesPerSecond / 1024.0 + return if (kb >= 1024) { + String.format("%.1f MB/s", kb / 1024.0) + } else { + String.format("%.1f KB/s", kb) + } + } + // --- 데이터 관리 및 이동 로직 (기존 유지) --- @RequiresApi(Build.VERSION_CODES.Q) @@ -337,9 +462,18 @@ class TorrentService : Service() { val swigParams = libtorrent.parse_magnet_uri(magnetUri, error) if (error.value() == 0) { swigParams.setSave_path(tempDir.absolutePath) - swigParams.flags = swigParams.flags.or_(libtorrent.getAuto_managed()) + + // 수동 제어를 위해 auto_managed 해제 + var flags = swigParams.flags + flags = flags.and_(libtorrent.getAuto_managed().inv()) + swigParams.flags = flags + session.swig().async_add_torrent(swigParams) + + // 추가 직후 상태 업데이트 호출하여 즉시 반영 + updateSessionState() } + updateTrackers() } catch (e: Exception) { e.printStackTrace() } }