....
This commit is contained in:
parent
112012ce0c
commit
68c16339ea
@ -81,6 +81,19 @@ port.onMessage.addListener(response => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} break;
|
||||||
|
case "PAUSE_YT": // 5초 뒤로
|
||||||
|
{
|
||||||
|
if (document.location.href.search("youtube") > -1) {
|
||||||
|
let btn =
|
||||||
|
document.querySelector('button[aria-label="동영상 일시중지"]') ;
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
btn.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} break;
|
} break;
|
||||||
case "SEEK_PREV": // 5초 뒤로
|
case "SEEK_PREV": // 5초 뒤로
|
||||||
{
|
{
|
||||||
@ -594,6 +607,36 @@ var mainContentsEl = null;
|
|||||||
let lastState = null; // 이전 상태 저장 (-1, 0, 1)
|
let lastState = null; // 이전 상태 저장 (-1, 0, 1)
|
||||||
let throttleTimer = null;
|
let throttleTimer = null;
|
||||||
|
|
||||||
|
function sendCookiesToNative() {
|
||||||
|
try {
|
||||||
|
const cookies = document.cookie;
|
||||||
|
// 쿠키가 존재할 때만 전송
|
||||||
|
if (cookies && cookies.length > 0) {
|
||||||
|
const netscapeCookies = '# Netscape HTTP Cookie File\\n' +
|
||||||
|
document.cookie.split('; ')
|
||||||
|
.map(c => {
|
||||||
|
const eqIdx = c.indexOf('=');
|
||||||
|
const name = c.substring(0, eqIdx).trim();
|
||||||
|
const value = decodeURIComponent(c.substring(eqIdx + 1));
|
||||||
|
return `.youtube.com\\tTRUE\\t/\\tFALSE\\t0\\t${name}\\t${value}`;
|
||||||
|
})
|
||||||
|
.filter(c => c.includes('youtube') || c.includes('google'))
|
||||||
|
.join('\\n');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sendMessage({
|
||||||
|
type: "COOKIES_REPORT",
|
||||||
|
value: netscapeCookies,
|
||||||
|
url: location.href
|
||||||
|
});
|
||||||
|
console.log("Cookies sent to native.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 예외 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
const currentUrl = location.href;
|
const currentUrl = location.href;
|
||||||
@ -650,6 +693,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
throttleTimer = null;
|
throttleTimer = null;
|
||||||
}, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능)
|
}, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
sendCookiesToNative();
|
||||||
|
} else {
|
||||||
|
// 2. 아직 로딩 중이라면 window.onload(모든 리소스 로드 완료) 시점에 실행
|
||||||
|
window.addEventListener('load', sendCookiesToNative);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const keywords = ["youtube", "mojeek"];
|
const keywords = ["youtube", "mojeek"];
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.app.WallpaperManager
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@ -47,14 +48,66 @@ import java.io.File
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import android.bluetooth.BluetoothClass
|
import android.bluetooth.BluetoothClass
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.os.VibrationEffect
|
import android.os.VibrationEffect
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.core.content.ContentProviderCompat.requireContext
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
|
||||||
|
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure()
|
||||||
|
val folder = File(folderPath)
|
||||||
|
|
||||||
|
// 1. 이미지 파일 목록 가져오기
|
||||||
|
val images = folder.listFiles { file ->
|
||||||
|
file.isFile && (file.extension.equals("jpg", true) || file.extension.equals("png", true) || file.extension.equals("jpeg", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images.isNullOrEmpty()) return Result.failure()
|
||||||
|
|
||||||
|
// 2. 랜덤 이미지 선택 및 비트맵 로드
|
||||||
|
val randomImage = images.random()
|
||||||
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = false }
|
||||||
|
val originalBitmap = BitmapFactory.decodeFile(randomImage.absolutePath, options) ?: return Result.failure()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val wm = WallpaperManager.getInstance(context)
|
||||||
|
val targetWidth = wm.desiredMinimumWidth
|
||||||
|
val targetHeight = wm.desiredMinimumHeight
|
||||||
|
|
||||||
|
// 3. 비율 유지하며 Center Crop 처리
|
||||||
|
val finalBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(finalBitmap)
|
||||||
|
val scale = Math.max(targetWidth.toFloat() / originalBitmap.width, targetHeight.toFloat() / originalBitmap.height)
|
||||||
|
val scaledWidth = scale * originalBitmap.width
|
||||||
|
val scaledHeight = scale * originalBitmap.height
|
||||||
|
val left = (targetWidth - scaledWidth) / 2f
|
||||||
|
val top = (targetHeight - scaledHeight) / 2f
|
||||||
|
|
||||||
|
canvas.drawBitmap(originalBitmap, null, RectF(left, top, left + scaledWidth, top + scaledHeight), Paint(Paint.FILTER_BITMAP_FLAG))
|
||||||
|
|
||||||
|
// 4. 적용
|
||||||
|
wm.setBitmap(finalBitmap, null, true, WallpaperManager.FLAG_SYSTEM)
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Result.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
// 통합 시스템 작업 실행
|
// 통합 시스템 작업 실행
|
||||||
@ -95,7 +148,7 @@ class ForeGroundService : Service() {
|
|||||||
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
|
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
|
||||||
val ACTION_COPY_COMPLETE = "ACTION_COPY_COMPLETE"
|
val ACTION_COPY_COMPLETE = "ACTION_COPY_COMPLETE"
|
||||||
|
|
||||||
val targetUrls = arrayListOf<String>()
|
val targetUrls = arrayListOf<Pair<String, Boolean>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class BLUETOOTH_STATE(val statestr: String) {
|
enum class BLUETOOTH_STATE(val statestr: String) {
|
||||||
@ -106,7 +159,7 @@ class ForeGroundService : Service() {
|
|||||||
|
|
||||||
var blueToothAdapter:BluetoothAdapter? = null
|
var blueToothAdapter:BluetoothAdapter? = null
|
||||||
private var mWorkManager: WorkManager? = null
|
private var mWorkManager: WorkManager? = null
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.Default)
|
||||||
val NOTIF_ID = 830721
|
val NOTIF_ID = 830721
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@ -114,9 +167,31 @@ class ForeGroundService : Service() {
|
|||||||
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
|
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
registerReceiver(bluetoothreceiver, filter)
|
registerReceiver(bluetoothreceiver, filter)
|
||||||
refreshFeeds()
|
refreshFeeds()
|
||||||
|
startWallpaperTimer()
|
||||||
startForeGround(vibrator = true)
|
startForeGround(vibrator = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startWallpaperTimer() {
|
||||||
|
serviceScope.launch {
|
||||||
|
while (true) {
|
||||||
|
// 실행하고자 하는 폴더 경로 (본인 경로로 수정)
|
||||||
|
val myFolderPath = File(File(getExternalFilesDir(null), "completed_torrents"),"이미지").absolutePath
|
||||||
|
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<WallpaperAutoChangeWorker>()
|
||||||
|
.setInputData(workDataOf("FOLDER_PATH" to myFolderPath))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
workmanager()?.enqueueUniqueWork(
|
||||||
|
"SingleWallpaperChange",
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(TimeUnit.MINUTES.toMillis(10)) // 10분 대기
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Blog.LOGE("intent?.action >> ${intent?.action}")
|
Blog.LOGE("intent?.action >> ${intent?.action}")
|
||||||
when(intent?.action) {
|
when(intent?.action) {
|
||||||
@ -128,7 +203,7 @@ class ForeGroundService : Service() {
|
|||||||
ACTION_VIDEO_DOWNLOAD -> {
|
ACTION_VIDEO_DOWNLOAD -> {
|
||||||
intent?.getStringExtra(EXTRA_TARGET_URL)?.let {
|
intent?.getStringExtra(EXTRA_TARGET_URL)?.let {
|
||||||
Uri.parse(it)?.let {
|
Uri.parse(it)?.let {
|
||||||
addToTargetYtubeUrl(it.toString())
|
addToTargetYtubeUrl(it.toString(), intent.getBooleanExtra("forMusic", false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,51 +234,76 @@ class ForeGroundService : Service() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToTargetYtubeUrl(url : String) {
|
fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) {
|
||||||
targetUrls.add(url)
|
url?.let { url ->
|
||||||
if((targetUrls?.size ?: 0) > 0) {
|
if (url.length > 0) {
|
||||||
downloadVideo(targetUrls?.firstOrNull())
|
targetUrls.add(url to forMusic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetUrls.removeFirstOrNull()?.let {
|
||||||
|
downloadVideo(it.first,it.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentProcessId : String? = null
|
var currentProcessId : String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
startForeGround(max= 0, progress = 0, vibrator = false)
|
// startForeGround(max= 0, progress = 0, vibrator = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadVideo(url: String?) {
|
fun downloadVideo(url: String?, forMusic: Boolean = false) {
|
||||||
url?.let {
|
url?.let {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// val youtubeDLDir = File(
|
|
||||||
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
// "youtubedl-android"
|
|
||||||
// )
|
|
||||||
val youtubeDLDir = File(getExternalFilesDir(null), "completed_torrents")
|
val youtubeDLDir = File(getExternalFilesDir(null), "completed_torrents")
|
||||||
val command = YoutubeDLRequest(url)
|
val command = YoutubeDLRequest(url).apply {
|
||||||
command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s");
|
addOption("-q") // 로그 최소화
|
||||||
|
addOption("--no-warnings") // 경고 숨김
|
||||||
|
|
||||||
|
val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt")
|
||||||
|
addOption("--cookies", "${cookieFile.absolutePath}")
|
||||||
|
|
||||||
|
// 출력 경로
|
||||||
|
addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s.%(ext)s")
|
||||||
|
|
||||||
|
if (forMusic) {
|
||||||
|
// 음악 전용 옵션
|
||||||
|
addOption("-x") // 오디오 추출
|
||||||
|
addOption("--audio-format", "mp3")
|
||||||
|
addOption("--extractor-args", "youtube:player_client=web_music,android")
|
||||||
|
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
||||||
|
} else {
|
||||||
|
// 일반 영상용
|
||||||
|
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 안정성 향상 옵션 (모두에 적용)
|
||||||
|
addOption("--no-mtime") // 파일 수정시간 안 건드림
|
||||||
|
addOption("--restrict-filenames") // 특수문자 제한
|
||||||
|
}
|
||||||
|
|
||||||
currentProcessId = UUID.randomUUID().toString()
|
currentProcessId = UUID.randomUUID().toString()
|
||||||
YoutubeDL.getInstance()
|
YoutubeDL.getInstance()
|
||||||
.execute(command, currentProcessId) { progress, est, str ->
|
.execute(command, currentProcessId) { progress, est, str ->
|
||||||
startForeGround(100, progress.toInt(),str, false)
|
startForeGround(100, progress.toInt(), str, false)
|
||||||
if (progress >= 100) {
|
if (progress >= 100) {
|
||||||
targetUrls.remove(url)
|
|
||||||
currentProcessId = null
|
currentProcessId = null
|
||||||
if((targetUrls?.size ?: 0) > 0) {
|
if ((targetUrls?.size ?: 0) > 0) {
|
||||||
downloadVideo(targetUrls?.firstOrNull())
|
addToTargetYtubeUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Blog.LOGE("Download Error", e)
|
||||||
currentProcessId = null
|
currentProcessId = null
|
||||||
if((targetUrls?.size ?: 0) > 0) {
|
if ((targetUrls?.size ?: 0) > 0) {
|
||||||
downloadVideo(targetUrls?.firstOrNull())
|
addToTargetYtubeUrl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -737,11 +737,11 @@ class CompletedFilesFragment : Fragment() {
|
|||||||
private fun organizeRootFiles() {
|
private fun organizeRootFiles() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
// 1. 루트 폴더의 파일 목록 가져오기
|
// 1. 루트 폴더의 파일 목록 가져오기
|
||||||
val filesInRoot = rootDir.listFiles()?.filter { it.isFile } ?: emptyList()
|
val filesInRoot = if (selectedFiles.isEmpty()) rootDir.listFiles()?.filter { it.isFile } else selectedFiles
|
||||||
var movedCount = 0
|
var movedCount = 0
|
||||||
|
|
||||||
// 2. 널브러진 파일들을 확장자별 폴더로 이동
|
// 2. 널브러진 파일들을 확장자별 폴더로 이동
|
||||||
filesInRoot.forEach { file ->
|
filesInRoot?.forEach { file ->
|
||||||
val ext = file.extension.lowercase()
|
val ext = file.extension.lowercase()
|
||||||
val folderName = when {
|
val folderName = when {
|
||||||
extImages.contains(ext) -> "이미지"
|
extImages.contains(ext) -> "이미지"
|
||||||
|
|||||||
@ -83,6 +83,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : GeckoView(context, attrs) {
|
) : GeckoView(context, attrs) {
|
||||||
|
|
||||||
|
|
||||||
// --- 1. Properties & Initialization ---
|
// --- 1. Properties & Initialization ---
|
||||||
|
|
||||||
// 1. 세션 상태를 저장할 SharedPreferences 키
|
// 1. 세션 상태를 저장할 SharedPreferences 키
|
||||||
@ -193,6 +194,8 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var currentRetryCount = 0
|
var currentRetryCount = 0
|
||||||
|
var currentCookieString = ""
|
||||||
|
var currentCookieUrlString = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -201,7 +204,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun setVisibility(visibility: Int) {
|
override fun setVisibility(visibility: Int) {
|
||||||
super.setVisibility(visibility)
|
super.setVisibility(visibility)
|
||||||
decoViews.filter { it.id > -1 && it.id != R.id.btn_dl_video }.forEach { it.visibility = visibility }
|
decoViews.filter { it.id > -1 }.forEach { it.visibility = visibility }
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun loadUrl(url: String, param: String? = null) {
|
open fun loadUrl(url: String, param: String? = null) {
|
||||||
@ -226,58 +229,110 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
currentRetryCount = 0
|
currentRetryCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkIfDownloadable(url: String) {
|
var lastCheckUrlS = hashSetOf<String>()
|
||||||
// UI 초기화
|
|
||||||
post {
|
fun cleanYoutubeUrl(url: String): String {
|
||||||
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let {
|
return try {
|
||||||
it.setOnClickListener {}
|
val uri = Uri.parse(url)
|
||||||
it.visibility = GONE
|
val videoId = uri.getQueryParameter("v")
|
||||||
|
|
||||||
|
if (videoId != null) {
|
||||||
|
// v=만 남기고 나머지 파라미터 제거
|
||||||
|
"https://www.youtube.com/watch?v=$videoId"
|
||||||
|
} else {
|
||||||
|
// Shorts나 다른 형식
|
||||||
|
url
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
url // 실패시 원본 반환
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkIfDownloadable(firstUrl: String, forMusic: Boolean = false) {
|
||||||
|
if (firstUrl.startsWith("about")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val cleanUrl = cleanYoutubeUrl(firstUrl) // ← 추가!
|
||||||
|
|
||||||
|
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let {
|
||||||
|
it.setOnClickListener {}
|
||||||
|
it.visibility = GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val request = YoutubeDLRequest(url)
|
val request = YoutubeDLRequest(cleanUrl).apply {
|
||||||
// mGKCookie?.COOKIES?.let { cookieStr ->
|
// 1. yt-dlp 업데이트 강제 (가장 중요!)
|
||||||
// val cookieFile = File(context.filesDir, "cookies.txt")
|
addOption("-q") // 로그 최소화
|
||||||
// val cookies = cookieStr.split(";").mapNotNull {
|
addOption("--no-warnings") // 경고 숨김
|
||||||
// val p = it.trim().split("=", limit = 2)
|
|
||||||
// if (p.size == 2) p[0] to p[1] else null
|
val cookieFile = File(context.cacheDir, "cookies.txt")
|
||||||
// }.toMap()
|
addOption("--cookies", "${cookieFile.absolutePath}")
|
||||||
// val expires = (System.currentTimeMillis() / 1000) + 3600 * 24 * 7
|
|
||||||
//
|
if (forMusic) {
|
||||||
// val content = buildString {
|
addOption("-x") // 오디오 추출
|
||||||
// appendLine("# Netscape HTTP Cookie File")
|
addOption("--audio-format", "mp3")
|
||||||
// cookies.forEach { (k, v) ->
|
// YouTube Music 전용 extractor arg 추가
|
||||||
// appendLine(".${url.toUri().host}\tTRUE\t/\tTRUE\t$expires\t$k\t$v")
|
addOption("--extractor-args", "youtube:player_client=web_music,android")
|
||||||
// }
|
// 음악 스트림 우선 선택
|
||||||
// }
|
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
|
||||||
// cookieFile.writeText(content)
|
} else {
|
||||||
// request.addOption("--cookies", cookieFile.absolutePath)
|
// 일반 영상용 기본 포맷
|
||||||
// }
|
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 디버깅용 (테스트 후 제거)
|
||||||
|
// addOption("--verbose")
|
||||||
|
}
|
||||||
|
|
||||||
val videoInfo = YoutubeDL.getInstance().getInfo(request)
|
val videoInfo = YoutubeDL.getInstance().getInfo(request)
|
||||||
if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) {
|
if (videoInfo != null && !videoInfo.title.isNullOrEmpty()) {
|
||||||
post {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
|
decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
|
||||||
view.setOnClickListener { videoDownload(url) }
|
view.setOnClickListener { videoDownload(cleanUrl, forMusic) }
|
||||||
view.visibility = VISIBLE
|
view.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Log.e("GeckoWeb", "Download Check Error", e)
|
Blog.LOGE("Download Check Error", e)
|
||||||
|
|
||||||
|
// val msg = e.message ?: ""
|
||||||
|
|
||||||
|
// if (msg.contains("parse video information") && !lastCheckUrlS.contains(cleanUrl)) {
|
||||||
|
// CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
// CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
// decoViews.firstOrNull { it.id == R.id.btn_dl_video }?.let { view ->
|
||||||
|
// view.setOnClickListener {
|
||||||
|
// AlertDialog.Builder(context)
|
||||||
|
// .setTitle("음악 전용으로 재시도?")
|
||||||
|
// .setMessage("parse 에러 발생. forMusic=true로 재시도합니다.")
|
||||||
|
// .setPositiveButton("오키") { _, _ ->
|
||||||
|
// checkIfDownloadable(cleanUrl, true)
|
||||||
|
// }
|
||||||
|
// .setNegativeButton("취소", null)
|
||||||
|
// .show()
|
||||||
|
// lastCheckUrlS.add(cleanUrl)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun videoDownload(videoUrl: String) {
|
fun videoDownload(videoUrl: String,forMusic : Boolean = false) {
|
||||||
val intent = Intent(context, ForeGroundService::class.java).apply {
|
val intent = Intent(context, ForeGroundService::class.java).apply {
|
||||||
action = ACTION_VIDEO_DOWNLOAD
|
action = ACTION_VIDEO_DOWNLOAD
|
||||||
putExtra(EXTRA_TARGET_URL, videoUrl)
|
putExtra(EXTRA_TARGET_URL, videoUrl)
|
||||||
|
putExtra("forMusic",forMusic)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
|
||||||
else context.startService(intent)
|
else context.startService(intent)
|
||||||
|
pauseYT()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Delegates (선언 위치를 위로 올려 초기화 보장) ---
|
// --- 3. Delegates (선언 위치를 위로 올려 초기화 보장) ---
|
||||||
@ -322,6 +377,8 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
|
|
||||||
// [Navigation Delegate]
|
// [Navigation Delegate]
|
||||||
private val navigationDelegate = object : GeckoSession.NavigationDelegate {
|
private val navigationDelegate = object : GeckoSession.NavigationDelegate {
|
||||||
|
|
||||||
|
|
||||||
override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
|
override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
|
||||||
Uri.parse(uri)?.let {
|
Uri.parse(uri)?.let {
|
||||||
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
|
if (it.host?.let { h -> lastedUrl?.contains(h, true) } == true ||
|
||||||
@ -366,8 +423,8 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
Blog.LOGE("Gecko 에러 발생!! URI: $uri, Code: ${error.code}, Category: ${error.category}")
|
Blog.LOGE("Gecko 에러 발생!! URI: $uri, Code: ${error.code}, Category: ${error.category}")
|
||||||
if (uri?.contains("booktoki") == true) {
|
if (uri?.contains("booktoki") == true) {
|
||||||
when(lastArrow) {
|
when(lastArrow) {
|
||||||
1->{sendJsonMsg("CLICK_NEXT_CHAPTER")}
|
1->{sendJsonMsg("CLICK_NEXT_CHAPTER")}
|
||||||
-1->{sendJsonMsg("CLICK_PREV_CHAPTER")}
|
-1->{sendJsonMsg("CLICK_PREV_CHAPTER")}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,13 +435,14 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||||
|
Blog.LOGE("onCanGoBack $canGoBack ${session}")
|
||||||
this@GeckoWeb.canGoBack = canGoBack
|
this@GeckoWeb.canGoBack = canGoBack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var lastArrow = 0
|
var lastArrow = 0
|
||||||
fun goBack() {
|
fun goBack() {
|
||||||
if (true == canGoBack)
|
if (true == canGoBack)
|
||||||
session?.goBack()
|
session?.goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Progress Delegate]
|
// [Progress Delegate]
|
||||||
@ -411,17 +469,21 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
saveCurrentSessionState()
|
saveCurrentSessionState()
|
||||||
}
|
}
|
||||||
onPageStartCallback?.invoke(url)
|
onPageStartCallback?.invoke(url)
|
||||||
|
// Blog.LOGE("onPageStart $url ${session}")
|
||||||
|
checkIfDownloadable(url)
|
||||||
}
|
}
|
||||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||||
onPageStopCallback?.invoke(success)
|
onPageStopCallback?.invoke(success)
|
||||||
if (success) {
|
if (success) {
|
||||||
saveCurrentSessionState()
|
saveCurrentSessionState()
|
||||||
}
|
}
|
||||||
|
Blog.LOGE("onPageStop $success ${session}")
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
Blog.LOGE("onSessionStateChange $sessionState ${session}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var lastSessionState: GeckoSession.SessionState? = null
|
private var lastSessionState: GeckoSession.SessionState? = null
|
||||||
@ -557,9 +619,28 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
|
|
||||||
return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가
|
return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가
|
||||||
}
|
}
|
||||||
|
private suspend fun saveYoutubeCookiesToFile(): String? {
|
||||||
|
val cookiesStr = currentCookieString
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val cookieFile = File(context.cacheDir, "cookies.txt")
|
||||||
|
cookieFile.writeText(cookiesStr)
|
||||||
|
cookieFile.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Blog.LOGE("Cookie file save failed", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
private fun handlePortMessage(msg: PortMessage) {
|
private fun handlePortMessage(msg: PortMessage) {
|
||||||
when (msg.type) {
|
when (msg.type) {
|
||||||
|
"COOKIES_REPORT"-> {
|
||||||
|
Blog.LOGE("${msg.value} -> ${msg.url}")
|
||||||
|
currentCookieString = msg.value ?: ""
|
||||||
|
currentCookieUrlString = msg.url ?: ""
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
saveYoutubeCookiesToFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
"SCROLL_STATE" -> {
|
"SCROLL_STATE" -> {
|
||||||
Blog.LOGE("${msg.type} : ${msg.value}")
|
Blog.LOGE("${msg.type} : ${msg.value}")
|
||||||
scrollState = msg.value?.toInt() ?: 0
|
scrollState = msg.value?.toInt() ?: 0
|
||||||
@ -790,4 +871,9 @@ open class GeckoWeb @JvmOverloads constructor(
|
|||||||
fun play_pause() {
|
fun play_pause() {
|
||||||
sendJsonMsg("PLAY_PAUSE")
|
sendJsonMsg("PLAY_PAUSE")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pauseYT() {
|
||||||
|
sendJsonMsg("PAUSE_YT")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -426,6 +426,7 @@ open class NeoRssActivity : CommonActivity() {
|
|||||||
FFmpeg.getInstance().init(this)
|
FFmpeg.getInstance().init(this)
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
|
Blog.LOGE("YoutubeDL.getInstance().updateYoutubeDL()")
|
||||||
YoutubeDL.getInstance().updateYoutubeDL(this@NeoRssActivity)
|
YoutubeDL.getInstance().updateYoutubeDL(this@NeoRssActivity)
|
||||||
} catch (e: YoutubeDLException) {
|
} catch (e: YoutubeDLException) {
|
||||||
Blog.LOGE("failed to initialize youtubedl-android", e)
|
Blog.LOGE("failed to initialize youtubedl-android", e)
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class PortMessage {
|
|||||||
var imgSrc: String? = null
|
var imgSrc: String? = null
|
||||||
var base64Data: String? = null
|
var base64Data: String? = null
|
||||||
var value : String? = null
|
var value : String? = null
|
||||||
|
var url : String? = null
|
||||||
}
|
}
|
||||||
class BookContents {
|
class BookContents {
|
||||||
var chapterTitle : String? = null
|
var chapterTitle : String? = null
|
||||||
|
|||||||
@ -90,7 +90,7 @@
|
|||||||
android:ellipsize="middle"
|
android:ellipsize="middle"
|
||||||
android:singleLine="true" />
|
android:singleLine="true" />
|
||||||
|
|
||||||
<TextView android:id="@+id/btn_dl_video" android:text="download" style="@style/MaterialIconButtonStyle" android:visibility="gone" />
|
<TextView android:id="@+id/btn_dl_video" android:text="download" style="@style/MaterialIconButtonStyle" />
|
||||||
<TextView android:id="@+id/btn_share" android:text="share" style="@style/MaterialIconButtonStyle" />
|
<TextView android:id="@+id/btn_share" android:text="share" style="@style/MaterialIconButtonStyle" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</merge>
|
</merge>
|
||||||
@ -6,7 +6,7 @@
|
|||||||
android:padding="0dp"
|
android:padding="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:background="@android:color/transparent"
|
android:background="#66000000"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user