...
This commit is contained in:
parent
fd0ee96c04
commit
ac1c0b12b3
@ -13,6 +13,10 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -46,6 +50,7 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import bums.lunatic.launcher.LunaticLauncher.Companion.isWifiConnected
|
||||||
import bums.lunatic.launcher.apps.AppDrawerBottomSheet
|
import bums.lunatic.launcher.apps.AppDrawerBottomSheet
|
||||||
import bums.lunatic.launcher.common.CommonActivity
|
import bums.lunatic.launcher.common.CommonActivity
|
||||||
import bums.lunatic.launcher.databinding.LauncherActivityBinding
|
import bums.lunatic.launcher.databinding.LauncherActivityBinding
|
||||||
@ -64,6 +69,7 @@ import bums.lunatic.launcher.receiver.NLService
|
|||||||
import bums.lunatic.launcher.receiver.SmsReceiver
|
import bums.lunatic.launcher.receiver.SmsReceiver
|
||||||
import bums.lunatic.launcher.settings.SettingsActivity
|
import bums.lunatic.launcher.settings.SettingsActivity
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import bums.lunatic.launcher.utils.Blog
|
||||||
|
import bums.lunatic.launcher.workers.BaseGetter
|
||||||
import bums.lunatic.launcher.workers.TorrentService
|
import bums.lunatic.launcher.workers.TorrentService
|
||||||
import bums.lunatic.launcher.workers.UsageLogType
|
import bums.lunatic.launcher.workers.UsageLogType
|
||||||
import bums.lunatic.launcher.workers.UsageUpdateType
|
import bums.lunatic.launcher.workers.UsageUpdateType
|
||||||
@ -580,8 +586,54 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
))
|
))
|
||||||
|
|
||||||
handleSharedIntent(intent)
|
handleSharedIntent(intent)
|
||||||
|
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val activeNetwork = connectivityManager.activeNetwork
|
||||||
|
val caps = connectivityManager.getNetworkCapabilities(activeNetwork)
|
||||||
|
LunaticLauncher.isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
|
registerNetworkCallback()
|
||||||
|
}
|
||||||
|
private fun registerNetworkCallback() {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
|
||||||
|
// 단순히 caps만 보지 말고, 현재 활성화된 기본 네트워크의 상태를 직접 다시 조회합니다.
|
||||||
|
val activeNet = connectivityManager.activeNetwork
|
||||||
|
val activeCaps = connectivityManager.getNetworkCapabilities(activeNet)
|
||||||
|
|
||||||
|
val wifiNow = activeCaps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
|
|
||||||
|
// 상태가 실제로 변했을 때만 업데이트하여 불필요한 로그와 로직 실행을 방지합니다.
|
||||||
|
if (isWifiConnected != wifiNow) {
|
||||||
|
isWifiConnected = wifiNow
|
||||||
|
|
||||||
|
}
|
||||||
|
val intent = Intent(this@LauncherActivity, TorrentService::class.java).apply {
|
||||||
|
putExtra("WIFI_STATE", isWifiConnected)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 네트워크가 완전히 끊겼을 때도 처리해주는 것이 안전합니다.
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
val intent = Intent(this@LauncherActivity, TorrentService::class.java).apply {
|
||||||
|
putExtra("WIFI_STATE", false)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
||||||
|
}
|
||||||
private var smsReceiver: SmsReceiver? = null
|
private var smsReceiver: SmsReceiver? = null
|
||||||
|
|
||||||
// 권한 요청 결과 처리기
|
// 권한 요청 결과 처리기
|
||||||
@ -911,12 +963,15 @@ open class LauncherActivity : CommonActivity() {
|
|||||||
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
|
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private lateinit var connectivityManager: ConnectivityManager
|
||||||
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
smsReceiver?.let {
|
smsReceiver?.let {
|
||||||
unregisterReceiver(it)
|
unregisterReceiver(it)
|
||||||
smsReceiver = null
|
smsReceiver = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,7 @@ internal class LunaticLauncher : Application() {
|
|||||||
companion object {
|
companion object {
|
||||||
var appContext : LunaticLauncher? = null
|
var appContext : LunaticLauncher? = null
|
||||||
var mHourlyLogWriter : HourlyLogWriter? = null
|
var mHourlyLogWriter : HourlyLogWriter? = null
|
||||||
|
var isWifiConnected : Boolean = false
|
||||||
private var sRuntime: GeckoRuntime? = null
|
private var sRuntime: GeckoRuntime? = null
|
||||||
fun getRuntime() : GeckoRuntime? {
|
fun getRuntime() : GeckoRuntime? {
|
||||||
appContext?.initGeckoRuntime()
|
appContext?.initGeckoRuntime()
|
||||||
|
|||||||
@ -77,6 +77,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
data class ZenQuoteResponse(val q: String, val a: String)
|
data class ZenQuoteResponse(val q: String, val a: String)
|
||||||
data class KorAdviceResponse(val author: String, val message: String)
|
data class KorAdviceResponse(val author: String, val message: String)
|
||||||
@ -515,6 +516,7 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) {
|
fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) {
|
||||||
|
Blog.LOGE("url $url")
|
||||||
url?.let { url ->
|
url?.let { url ->
|
||||||
if (url.length > 0) {
|
if (url.length > 0) {
|
||||||
targetUrls.add(url to forMusic)
|
targetUrls.add(url to forMusic)
|
||||||
@ -537,6 +539,7 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastProGress = -1f
|
||||||
fun downloadVideo(url: String?, forMusic: Boolean = false) {
|
fun downloadVideo(url: String?, forMusic: Boolean = false) {
|
||||||
url?.let {
|
url?.let {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@ -547,35 +550,42 @@ class ForeGroundService : Service() {
|
|||||||
val youtubeDLDir = File(baseDir, "Youtube")
|
val youtubeDLDir = File(baseDir, "Youtube")
|
||||||
if (!youtubeDLDir.exists()) youtubeDLDir.mkdirs()
|
if (!youtubeDLDir.exists()) youtubeDLDir.mkdirs()
|
||||||
val command = YoutubeDLRequest(url).apply {
|
val command = YoutubeDLRequest(url).apply {
|
||||||
addOption("-q") // 로그 최소화
|
addOption("--newline") // 줄바꿈 단위로 출력 (콜백 트리거 핵심)
|
||||||
|
addOption("--progress") // 진행 상태 강제 출력 // 로그 최소화
|
||||||
addOption("--no-warnings") // 경고 숨김
|
addOption("--no-warnings") // 경고 숨김
|
||||||
|
|
||||||
val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt")
|
val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt")
|
||||||
addOption("--cookies", "${cookieFile.absolutePath}")
|
addOption("--cookies", "${cookieFile.absolutePath}")
|
||||||
|
|
||||||
// 출력 경로
|
// 출력 경로
|
||||||
addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s.%(ext)s")
|
addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s [%(id)s].%(ext)s")
|
||||||
|
|
||||||
if (forMusic) {
|
// if (forMusic) {
|
||||||
// 음악 전용 옵션
|
// // 음악 전용 옵션
|
||||||
addOption("-x") // 오디오 추출
|
// addOption("-x") // 오디오 추출
|
||||||
addOption("--audio-format", "mp3")
|
// addOption("--audio-format", "mp3")
|
||||||
addOption("--extractor-args", "youtube:player_client=web_music,android")
|
// addOption("--extractor-args", "youtube:player_client=web_music,android")
|
||||||
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
// addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
||||||
} else {
|
// } else {
|
||||||
// 일반 영상용
|
// 일반 영상용
|
||||||
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
||||||
}
|
// }
|
||||||
|
addOption("--no-check-certificates")
|
||||||
// 안정성 향상 옵션 (모두에 적용)
|
addOption("--verbose")
|
||||||
addOption("--no-mtime") // 파일 수정시간 안 건드림
|
addOption("--no-mtime") // 파일 수정시간 안 건드림
|
||||||
|
addOption("--no-continue") // 이어받기 하지 않음
|
||||||
|
addOption("--force-overwrites") // 강제 덮어쓰기
|
||||||
addOption("--restrict-filenames") // 특수문자 제한
|
addOption("--restrict-filenames") // 특수문자 제한
|
||||||
}
|
}
|
||||||
|
|
||||||
currentProcessId = UUID.randomUUID().toString()
|
currentProcessId = UUID.randomUUID().toString()
|
||||||
YoutubeDL.getInstance()
|
var response = YoutubeDL.getInstance()
|
||||||
.execute(command, currentProcessId) { progress, est, str ->
|
.execute(command, currentProcessId) { progress, est, str ->
|
||||||
|
// Blog.LOGE("progress $progress est $est str $str" )
|
||||||
|
if(progress == 100.0f || progress == 0.0f || abs(lastProGress - progress) > 3) {
|
||||||
startForeGround(100, progress.toInt(), str, false)
|
startForeGround(100, progress.toInt(), str, false)
|
||||||
|
lastProGress = progress
|
||||||
|
}
|
||||||
if (progress >= 100) {
|
if (progress >= 100) {
|
||||||
currentProcessId = null
|
currentProcessId = null
|
||||||
if ((targetUrls?.size ?: 0) > 0) {
|
if ((targetUrls?.size ?: 0) > 0) {
|
||||||
@ -583,6 +593,10 @@ class ForeGroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Blog.LOGE("url $url $currentProcessId")
|
||||||
|
Blog.LOGE("Exit Code: ${response.exitCode}")
|
||||||
|
Blog.LOGE("Out: ${response.out}")
|
||||||
|
Blog.LOGE("Error: ${response.err}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Blog.LOGE("Download Error", e)
|
Blog.LOGE("Download Error", e)
|
||||||
currentProcessId = null
|
currentProcessId = null
|
||||||
@ -710,7 +724,7 @@ class ForeGroundService : Service() {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
// 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간)
|
// 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간)
|
||||||
val newsRequest = PeriodicWorkRequestBuilder<AggregatedNewsWorker>(PrefLong.shortTimePeriod.get(20L), TimeUnit.MINUTES)
|
val newsRequest = PeriodicWorkRequestBuilder<AggregatedNewsWorker>(PrefLong.longTimePeriod.get(120L), TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// 기존의 수많은 enqueue 코드를 이 두 개로 대체
|
// 기존의 수많은 enqueue 코드를 이 두 개로 대체
|
||||||
|
|||||||
@ -891,12 +891,9 @@ class CompletedFilesFragment : Fragment() {
|
|||||||
val innerFiles = folder.listFiles() ?: return@forEach
|
val innerFiles = folder.listFiles() ?: return@forEach
|
||||||
|
|
||||||
// 특수 조건(1GB 영상) 확인용 데이터
|
// 특수 조건(1GB 영상) 확인용 데이터
|
||||||
val videoFiles = innerFiles.filter { extVideos.contains(it.extension.lowercase()) }
|
val videoFiles = innerFiles.filter { extVideos.contains(it.extension.lowercase()) }.filter { it.length() >= 1024 * 1024 * 1024 }
|
||||||
val potentialSubtitles = innerFiles.filter { subtitleExts.contains(it.extension.lowercase()) }
|
val potentialSubtitles = innerFiles.filter { subtitleExts.contains(it.extension.lowercase()) }
|
||||||
val hasLargeVideo = videoFiles.any { it.length() >= 1024 * 1024 * 1024 }
|
if (videoFiles.isNotEmpty()) {
|
||||||
val hasTinyText = potentialSubtitles.any { it.length() <= 1024 }
|
|
||||||
|
|
||||||
if (hasLargeVideo) {
|
|
||||||
// 조건 만족 시 영상+자막 이동
|
// 조건 만족 시 영상+자막 이동
|
||||||
videoFiles.forEach { videoFile ->
|
videoFiles.forEach { videoFile ->
|
||||||
if (videoFile.renameTo(File(videoTargetDir, videoFile.name))) {
|
if (videoFile.renameTo(File(videoTargetDir, videoFile.name))) {
|
||||||
|
|||||||
@ -282,17 +282,17 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
val cookieFile = File(context.cacheDir, "cookies.txt")
|
val cookieFile = File(context.cacheDir, "cookies.txt")
|
||||||
addOption("--cookies", "${cookieFile.absolutePath}")
|
addOption("--cookies", "${cookieFile.absolutePath}")
|
||||||
|
|
||||||
if (forMusic) {
|
// if (forMusic) {
|
||||||
addOption("-x") // 오디오 추출
|
// addOption("-x") // 오디오 추출
|
||||||
addOption("--audio-format", "mp3")
|
// addOption("--audio-format", "mp3")
|
||||||
// YouTube Music 전용 extractor arg 추가
|
// // YouTube Music 전용 extractor arg 추가
|
||||||
addOption("--extractor-args", "youtube:player_client=web_music,android")
|
// addOption("--extractor-args", "youtube:player_client=web_music,android")
|
||||||
// 음악 스트림 우선 선택
|
// // 음악 스트림 우선 선택
|
||||||
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
// addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
||||||
} else {
|
// } else {
|
||||||
// 일반 영상용 기본 포맷
|
// 일반 영상용 기본 포맷
|
||||||
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
// 디버깅용 (테스트 후 제거)
|
// 디버깅용 (테스트 후 제거)
|
||||||
@ -476,6 +476,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
|
override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
|
||||||
lastSessionState = sessionState
|
lastSessionState = sessionState
|
||||||
onSessionStateChangeCallback?.invoke(sessionState)
|
onSessionStateChangeCallback?.invoke(sessionState)
|
||||||
|
saveCurrentSessionState()
|
||||||
// Blog.LOGE("onSessionStateChange $sessionState ${session}")
|
// Blog.LOGE("onSessionStateChange $sessionState ${session}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -661,7 +662,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"COOKIES_REPORT"-> {
|
"COOKIES_REPORT"-> {
|
||||||
// Blog.LOGE("${msg.value} -> ${msg.url}")
|
Blog.LOGE("${msg.value} -> ${msg.url}")
|
||||||
currentCookieString = msg.value ?: ""
|
currentCookieString = msg.value ?: ""
|
||||||
currentCookieUrlString = msg.url ?: ""
|
currentCookieUrlString = msg.url ?: ""
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|||||||
@ -496,6 +496,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
try {
|
try {
|
||||||
YoutubeDL.getInstance().init(this)
|
YoutubeDL.getInstance().init(this)
|
||||||
|
|
||||||
FFmpeg.getInstance().init(this)
|
FFmpeg.getInstance().init(this)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -149,11 +149,14 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
if (allSubtitleTracks.size > 1) {
|
if (allSubtitleTracks.size > 1) {
|
||||||
showSubtitleSelectionDialog()
|
showSubtitleSelectionDialog()
|
||||||
} else {
|
} else {
|
||||||
|
if (videoPath.contains("Youtube")) {
|
||||||
|
play()
|
||||||
|
} else {
|
||||||
showSubtitleSearchConfirmDialog()
|
showSubtitleSearchConfirmDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 💡 C++에서 오류 발생 시 호출
|
// 💡 C++에서 오류 발생 시 호출
|
||||||
onErrorListener = { errorCode, errorMessage ->
|
onErrorListener = { errorCode, errorMessage ->
|
||||||
@ -368,10 +371,28 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
val center = gestureLayer.getChildAt(1)
|
val center = gestureLayer.getChildAt(1)
|
||||||
val right = gestureLayer.getChildAt(2)
|
val right = gestureLayer.getChildAt(2)
|
||||||
|
|
||||||
center.setOnClickListener {
|
val centerDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
isPlaying = !isPlaying
|
isPlaying = !isPlaying
|
||||||
if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!))
|
if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!))
|
||||||
else nativePlayer?.pause()
|
else nativePlayer?.pause()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 좌우 스크롤로 자막 싱크 조절
|
||||||
|
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||||
|
if (Math.abs(distanceX) > Math.abs(distanceY)) {
|
||||||
|
if (distanceX > 0) adjustSubtitleSync(-500) // 왼쪽으로 밀면 자막을 빠르게
|
||||||
|
else adjustSubtitleSync(500) // 오른쪽으로 밀면 자막을 느리게
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
center.setOnTouchListener { _, event ->
|
||||||
|
centerDetector.onTouchEvent(event)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
@ -411,7 +432,17 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private var subtitleDelayMs: Long = 0L
|
||||||
|
private fun adjustSubtitleSync(deltaMs: Long) {
|
||||||
|
subtitleDelayMs += deltaMs
|
||||||
|
val seconds = subtitleDelayMs / 1000.0
|
||||||
|
val sign = if (subtitleDelayMs >= 0) "+" else ""
|
||||||
|
Toast.makeText(this, "자막 싱크: $sign${String.format("%.1f", seconds)}초", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// 싱크가 변경되면 즉시 루프에서 반영되므로 별도의 처리는 필요 없으나,
|
||||||
|
// 즉각적인 피드백을 위해 lastSubTitle을 초기화하여 강제 갱신 유도 가능
|
||||||
|
lastSubTitle = ""
|
||||||
|
}
|
||||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
hideSystemUI()
|
hideSystemUI()
|
||||||
@ -640,23 +671,21 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
* 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다.
|
* 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다.
|
||||||
*/
|
*/
|
||||||
private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? {
|
private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? {
|
||||||
// 1. 영상이 뒤로 감기 되었을 경우 인덱스 초기화
|
// 💡 싱크 오프셋 적용 (초 단위로 변환하여 더함)
|
||||||
|
val adjustedSec = currentSec - (subtitleDelayMs / 1000.0)
|
||||||
|
|
||||||
if (currentSubtitleIndex >= externalSubtitles.size ||
|
if (currentSubtitleIndex >= externalSubtitles.size ||
|
||||||
externalSubtitles[currentSubtitleIndex].startSec > currentSec) {
|
externalSubtitles[currentSubtitleIndex].startSec > adjustedSec) {
|
||||||
currentSubtitleIndex = 0
|
currentSubtitleIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 마지막으로 찾았던 위치(currentSubtitleIndex)부터 탐색 시작
|
|
||||||
for (i in currentSubtitleIndex until externalSubtitles.size) {
|
for (i in currentSubtitleIndex until externalSubtitles.size) {
|
||||||
val item = externalSubtitles[i]
|
val item = externalSubtitles[i]
|
||||||
|
if (adjustedSec in item.startSec..item.endSec) {
|
||||||
if (currentSec in item.startSec..item.endSec) {
|
currentSubtitleIndex = i
|
||||||
currentSubtitleIndex = i // 현재 위치 저장
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
if (item.startSec > adjustedSec) break
|
||||||
// 3. 자막이 시간순으로 정렬되어 있다면, 현재 시간보다 시작 시간이 커지는 순간 루프 종료
|
|
||||||
if (item.startSec > currentSec) break
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ abstract class BaseGetter(internal val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val USAGT = "Mozilla/5.0 (Android 15; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0"
|
val USAGT = "Mozilla/5.0 (Android 15; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0"
|
||||||
|
|
||||||
val limitDateTime = beforeOneDay()
|
val limitDateTime = beforeOneDay()
|
||||||
@ -32,6 +34,7 @@ abstract class BaseGetter(internal val context: Context) {
|
|||||||
|
|
||||||
abstract fun realWork() : List<RealmObject>
|
abstract fun realWork() : List<RealmObject>
|
||||||
open suspend fun fetchData(): List<RealmObject> {
|
open suspend fun fetchData(): List<RealmObject> {
|
||||||
|
|
||||||
return realWork()
|
return realWork()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,11 +93,8 @@ object TaskAggregator {
|
|||||||
|
|
||||||
// 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨)
|
// 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨)
|
||||||
val jobs = listOf(
|
val jobs = listOf(
|
||||||
// async { RuliWebGetter(context).fetchData() },
|
|
||||||
async { TheQooGetter(context).fetchData() },
|
async { TheQooGetter(context).fetchData() },
|
||||||
// async { YoutubeGetter(context).fetchData() },
|
|
||||||
async { DCGetter(context).fetchData() },
|
async { DCGetter(context).fetchData() },
|
||||||
// async { FmKoreaGetter(context).fetchData() },
|
|
||||||
async { NewsFeedsGetter(context).fetchData() },
|
async { NewsFeedsGetter(context).fetchData() },
|
||||||
async { ClienGetter(context).fetchData() },
|
async { ClienGetter(context).fetchData() },
|
||||||
async { DotaxGetter(context).fetchData() },
|
async { DotaxGetter(context).fetchData() },
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import com.frostwire.jlibtorrent.swig.settings_pack
|
|||||||
import com.frostwire.jlibtorrent.swig.torrent_status
|
import com.frostwire.jlibtorrent.swig.torrent_status
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
data class TorrentTask(
|
data class TorrentTask(
|
||||||
val infoHash: String,
|
val infoHash: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -45,8 +45,7 @@ class TorrentService : Service() {
|
|||||||
// 제어 플래그
|
// 제어 플래그
|
||||||
private var isWifiConnected = false
|
private var isWifiConnected = false
|
||||||
private var isCharging = false
|
private var isCharging = false
|
||||||
private lateinit var connectivityManager: ConnectivityManager
|
|
||||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
||||||
|
|
||||||
inner class TorrentBinder : Binder() {
|
inner class TorrentBinder : Binder() {
|
||||||
fun getService(): TorrentService = this@TorrentService
|
fun getService(): TorrentService = this@TorrentService
|
||||||
@ -56,6 +55,12 @@ class TorrentService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (!isKoreaRegion()) {
|
||||||
|
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
startForegroundService()
|
startForegroundService()
|
||||||
initLibTorrent()
|
initLibTorrent()
|
||||||
|
|
||||||
@ -64,7 +69,7 @@ class TorrentService : Service() {
|
|||||||
|
|
||||||
// 2. 리시버 및 콜백 등록
|
// 2. 리시버 및 콜백 등록
|
||||||
registerBatteryReceiver()
|
registerBatteryReceiver()
|
||||||
registerNetworkCallback()
|
|
||||||
|
|
||||||
// 3. 초기 세션 상태 적용
|
// 3. 초기 세션 상태 적용
|
||||||
updateSessionState()
|
updateSessionState()
|
||||||
@ -79,11 +84,7 @@ class TorrentService : Service() {
|
|||||||
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
||||||
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
|
||||||
// Wi-Fi 상태
|
|
||||||
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val activeNetwork = connectivityManager.activeNetwork
|
|
||||||
val caps = connectivityManager.getNetworkCapabilities(activeNetwork)
|
|
||||||
isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerBatteryReceiver() {
|
private fun registerBatteryReceiver() {
|
||||||
@ -151,34 +152,7 @@ class TorrentService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerNetworkCallback() {
|
|
||||||
val request = NetworkRequest.Builder()
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
||||||
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
|
|
||||||
// 단순히 caps만 보지 말고, 현재 활성화된 기본 네트워크의 상태를 직접 다시 조회합니다.
|
|
||||||
val activeNet = connectivityManager.activeNetwork
|
|
||||||
val activeCaps = connectivityManager.getNetworkCapabilities(activeNet)
|
|
||||||
|
|
||||||
val wifiNow = activeCaps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
|
||||||
|
|
||||||
// 상태가 실제로 변했을 때만 업데이트하여 불필요한 로그와 로직 실행을 방지합니다.
|
|
||||||
if (isWifiConnected != wifiNow) {
|
|
||||||
isWifiConnected = wifiNow
|
|
||||||
updateSessionState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 네트워크가 완전히 끊겼을 때도 처리해주는 것이 안전합니다.
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
checkInitialStatus() // 전체 상태 다시 체크
|
|
||||||
updateSessionState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var lastUpdateTime = -1L
|
var lastUpdateTime = -1L
|
||||||
@ -186,6 +160,15 @@ class TorrentService : Service() {
|
|||||||
* 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절
|
* 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절
|
||||||
*/
|
*/
|
||||||
private fun updateSessionState() {
|
private fun updateSessionState() {
|
||||||
|
if (!isKoreaRegion()) {
|
||||||
|
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIpAndStop()
|
||||||
|
|
||||||
if (session.isPaused) session.resume()
|
if (session.isPaused) session.resume()
|
||||||
var curentTime = System.currentTimeMillis()
|
var curentTime = System.currentTimeMillis()
|
||||||
if (curentTime - lastUpdateTime > 5000) {
|
if (curentTime - lastUpdateTime > 5000) {
|
||||||
@ -247,7 +230,38 @@ class TorrentService : Service() {
|
|||||||
refreshTorrentStats()
|
refreshTorrentStats()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isKoreaRegion(): Boolean {
|
||||||
|
val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||||
|
|
||||||
|
// 1. SIM 카드의 국가 코드 확인 (예: "kr")
|
||||||
|
val simCountry = tm.simCountryIso.lowercase()
|
||||||
|
|
||||||
|
// 2. 네트워크(기지국) 기준 국가 코드 확인
|
||||||
|
val networkCountry = tm.networkCountryIso.lowercase()
|
||||||
|
|
||||||
|
// SIM이나 네트워크 중 하나라도 'kr'이면 한국으로 판단
|
||||||
|
return simCountry == "kr" || networkCountry == "kr"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkIpAndStop() {
|
||||||
|
serviceScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// 외부 API를 통해 국가 코드 확인 (예: ip-api.com)
|
||||||
|
val response = java.net.URL("http://ip-api.com/json/").readText()
|
||||||
|
if (!response.contains("\"countryCode\":\"KR\"")) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Blog.LOGE("IP 위치가 대한민국이 아닙니다. 다운로드를 중지합니다.")
|
||||||
|
// 모든 토렌트 일시정지 또는 서비스 종료
|
||||||
|
session.pause()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 네트워크 오류 시 보수적으로 처리 (선택 사항)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TRACKER_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt"
|
private val TRACKER_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt"
|
||||||
@ -518,6 +532,10 @@ class TorrentService : Service() {
|
|||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) }
|
intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) }
|
||||||
|
intent?.getBooleanExtra("WIFI_STATE", false)?.let {
|
||||||
|
isWifiConnected = it
|
||||||
|
|
||||||
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,7 +580,6 @@ class TorrentService : Service() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
unregisterReceiver(batteryReceiver)
|
unregisterReceiver(batteryReceiver)
|
||||||
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
|
|
||||||
serviceScope.cancel()
|
serviceScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user