mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-03 16:04:30 +02:00
refactor: download
- download task manager - fix installation summary update
This commit is contained in:
parent
2ff8a69403
commit
3df11aadb8
@ -8,6 +8,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
||||||
import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
|
import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
|
||||||
import me.rhunk.snapenhance.core.config.ModConfig
|
import me.rhunk.snapenhance.core.config.ModConfig
|
||||||
|
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||||
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||||
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
|
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
|
||||||
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
|
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
|
||||||
@ -17,24 +18,40 @@ import java.lang.ref.WeakReference
|
|||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class RemoteSideContext(
|
class RemoteSideContext(
|
||||||
val androidContext: Context
|
ctx: Context
|
||||||
) {
|
) {
|
||||||
|
private var _context: WeakReference<Context> = WeakReference(ctx)
|
||||||
private var _activity: WeakReference<Activity>? = null
|
private var _activity: WeakReference<Activity>? = null
|
||||||
|
|
||||||
|
var androidContext: Context
|
||||||
|
get() = synchronized(this) {
|
||||||
|
_context.get() ?: error("Context is null")
|
||||||
|
}
|
||||||
|
set(value) { synchronized(this) {
|
||||||
|
_context.clear(); _context = WeakReference(value)
|
||||||
|
} }
|
||||||
|
|
||||||
var activity: Activity?
|
var activity: Activity?
|
||||||
get() = _activity?.get()
|
get() = _activity?.get()
|
||||||
set(value) { _activity = WeakReference(value) }
|
set(value) { _activity?.clear(); _activity = WeakReference(value) }
|
||||||
|
|
||||||
val config = ModConfig()
|
val config = ModConfig()
|
||||||
val translation = LocaleWrapper()
|
val translation = LocaleWrapper()
|
||||||
val mappings = MappingsWrapper(androidContext)
|
val mappings = MappingsWrapper(androidContext)
|
||||||
|
val downloadTaskManager = DownloadTaskManager()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
config.loadFromContext(androidContext)
|
runCatching {
|
||||||
translation.userLocale = config.locale
|
config.loadFromContext(androidContext)
|
||||||
translation.loadFromContext(androidContext)
|
translation.userLocale = config.locale
|
||||||
mappings.apply {
|
translation.loadFromContext(androidContext)
|
||||||
loadFromContext(androidContext)
|
mappings.apply {
|
||||||
init()
|
loadFromContext(androidContext)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
downloadTaskManager.init(androidContext)
|
||||||
|
}.onFailure {
|
||||||
|
Logger.error("Failed to initialize RemoteSideContext", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
package me.rhunk.snapenhance
|
package me.rhunk.snapenhance
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
object SharedContextHolder {
|
object SharedContextHolder {
|
||||||
private lateinit var _remoteSideContext: WeakReference<RemoteSideContext>
|
private lateinit var _remoteSideContext: RemoteSideContext
|
||||||
|
|
||||||
fun remote(context: Context): RemoteSideContext {
|
fun remote(context: Context): RemoteSideContext {
|
||||||
if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) {
|
if (!::_remoteSideContext.isInitialized) {
|
||||||
_remoteSideContext = WeakReference(RemoteSideContext(context.applicationContext))
|
_remoteSideContext = RemoteSideContext(context)
|
||||||
}
|
}
|
||||||
return _remoteSideContext.get()!!
|
|
||||||
|
if (_remoteSideContext.androidContext != context) {
|
||||||
|
_remoteSideContext.androidContext = context
|
||||||
|
}
|
||||||
|
|
||||||
|
return _remoteSideContext
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,11 +4,10 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.SharedContext
|
|
||||||
import me.rhunk.snapenhance.SharedContextHolder
|
import me.rhunk.snapenhance.SharedContextHolder
|
||||||
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.bridge.types.BridgeFileType
|
||||||
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
|
|
||||||
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
||||||
|
import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper
|
||||||
import me.rhunk.snapenhance.download.DownloadProcessor
|
import me.rhunk.snapenhance.download.DownloadProcessor
|
||||||
|
|
||||||
class BridgeService : Service() {
|
class BridgeService : Service() {
|
||||||
@ -105,10 +104,10 @@ class BridgeService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
|
override fun enqueueDownload(intent: Intent, callback: DownloadCallback) {
|
||||||
SharedContextHolder.remote(this@BridgeService)
|
DownloadProcessor(
|
||||||
//TODO: refactor shared context
|
remoteSideContext = SharedContextHolder.remote(this@BridgeService),
|
||||||
SharedContext.ensureInitialized(this@BridgeService)
|
callback = callback
|
||||||
DownloadProcessor(this@BridgeService, callback).onReceive(intent)
|
).onReceive(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package me.rhunk.snapenhance.download
|
package me.rhunk.snapenhance.download
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -16,17 +15,17 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import me.rhunk.snapenhance.Constants
|
import me.rhunk.snapenhance.Constants
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.SharedContext
|
import me.rhunk.snapenhance.SharedContext
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback
|
import me.rhunk.snapenhance.bridge.DownloadCallback
|
||||||
import me.rhunk.snapenhance.core.config.ModConfig
|
|
||||||
import me.rhunk.snapenhance.data.FileType
|
import me.rhunk.snapenhance.data.FileType
|
||||||
|
import me.rhunk.snapenhance.download.data.DownloadMediaType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
||||||
import me.rhunk.snapenhance.download.data.DownloadRequest
|
import me.rhunk.snapenhance.download.data.DownloadRequest
|
||||||
|
import me.rhunk.snapenhance.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
import me.rhunk.snapenhance.download.data.InputMedia
|
||||||
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
||||||
import me.rhunk.snapenhance.download.data.PendingDownload
|
import me.rhunk.snapenhance.download.data.PendingDownload
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
|
||||||
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
|
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
|
||||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -56,7 +55,7 @@ data class DownloadedFile(
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
class DownloadProcessor (
|
class DownloadProcessor (
|
||||||
private val context: Context,
|
private val remoteSideContext: RemoteSideContext,
|
||||||
private val callback: DownloadCallback
|
private val callback: DownloadCallback
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -69,11 +68,29 @@ class DownloadProcessor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fallbackToast(message: Any) {
|
private fun fallbackToast(message: Any) {
|
||||||
android.os.Handler(context.mainLooper).post {
|
android.os.Handler(remoteSideContext.androidContext.mainLooper).post {
|
||||||
Toast.makeText(context, message.toString(), Toast.LENGTH_SHORT).show()
|
Toast.makeText(remoteSideContext.androidContext, message.toString(), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun callbackOnSuccess(path: String) = runCatching {
|
||||||
|
callback.onSuccess(path)
|
||||||
|
}.onFailure {
|
||||||
|
fallbackToast(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callbackOnFailure(message: String, throwable: String? = null) = runCatching {
|
||||||
|
callback.onFailure(message, throwable)
|
||||||
|
}.onFailure {
|
||||||
|
fallbackToast("$message\n$throwable")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun callbackOnProgress(message: String) = runCatching {
|
||||||
|
callback.onProgress(message)
|
||||||
|
}.onFailure {
|
||||||
|
fallbackToast(it)
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractZip(inputStream: InputStream): List<File> {
|
private fun extractZip(inputStream: InputStream): List<File> {
|
||||||
val files = mutableListOf<File>()
|
val files = mutableListOf<File>()
|
||||||
val zipInputStream = ZipInputStream(inputStream)
|
val zipInputStream = ZipInputStream(inputStream)
|
||||||
@ -100,30 +117,20 @@ class DownloadProcessor (
|
|||||||
return CipherInputStream(inputStream, cipher)
|
return CipherInputStream(inputStream, cipher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNeededDirectories(file: File): File {
|
|
||||||
val directory = file.parentFile ?: return file
|
|
||||||
if (!directory.exists()) {
|
|
||||||
directory.mkdirs()
|
|
||||||
}
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
||||||
if (coroutineContext.job.isCancelled) return
|
if (coroutineContext.job.isCancelled) return
|
||||||
|
|
||||||
val config by ModConfig().apply { loadFromContext(context) }
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val fileType = FileType.fromFile(inputFile)
|
val fileType = FileType.fromFile(inputFile)
|
||||||
if (fileType == FileType.UNKNOWN) {
|
if (fileType == FileType.UNKNOWN) {
|
||||||
callback.onFailure(translation.format("failed_gallery_toast", "error" to "Unknown media type"), null)
|
callbackOnFailure(translation.format("failed_gallery_toast", "error" to "Unknown media type"), null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension
|
val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension
|
||||||
|
|
||||||
val outputFolder = DocumentFile.fromTreeUri(context, Uri.parse(config.downloader.saveFolder.get()))
|
val outputFolder = DocumentFile.fromTreeUri(remoteSideContext.androidContext, Uri.parse(remoteSideContext.config.root.downloader.saveFolder.get()))
|
||||||
?: throw Exception("Failed to open output folder")
|
?: throw Exception("Failed to open output folder")
|
||||||
|
|
||||||
val outputFileFolder = pendingDownload.metadata.outputPath.let {
|
val outputFileFolder = pendingDownload.metadata.outputPath.let {
|
||||||
@ -137,7 +144,7 @@ class DownloadProcessor (
|
|||||||
}
|
}
|
||||||
|
|
||||||
val outputFile = outputFileFolder.createFile(fileType.mimeType, fileName)!!
|
val outputFile = outputFileFolder.createFile(fileType.mimeType, fileName)!!
|
||||||
val outputStream = context.contentResolver.openOutputStream(outputFile.uri)!!
|
val outputStream = remoteSideContext.androidContext.contentResolver.openOutputStream(outputFile.uri)!!
|
||||||
|
|
||||||
inputFile.inputStream().use { inputStream ->
|
inputFile.inputStream().use { inputStream ->
|
||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
@ -149,21 +156,17 @@ class DownloadProcessor (
|
|||||||
runCatching {
|
runCatching {
|
||||||
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
|
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
|
||||||
mediaScanIntent.setData(outputFile.uri)
|
mediaScanIntent.setData(outputFile.uri)
|
||||||
context.sendBroadcast(mediaScanIntent)
|
remoteSideContext.androidContext.sendBroadcast(mediaScanIntent)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Logger.error("Failed to scan media file", it)
|
Logger.error("Failed to scan media file", it)
|
||||||
callback.onFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message)
|
callbackOnFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug("download complete")
|
Logger.debug("download complete")
|
||||||
fileName.let {
|
callbackOnSuccess(fileName)
|
||||||
runCatching { callback.onSuccess(it) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
translation.format("failed_gallery_toast", "error" to exception.toString()).let {
|
callbackOnFailure(translation.format("failed_gallery_toast", "error" to exception.toString()), exception.message)
|
||||||
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
pendingDownload.downloadStage = DownloadStage.FAILED
|
pendingDownload.downloadStage = DownloadStage.FAILED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,9 +253,7 @@ class DownloadProcessor (
|
|||||||
val xmlData = dashPlaylistFile.outputStream()
|
val xmlData = dashPlaylistFile.outputStream()
|
||||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(playlistXml), StreamResult(xmlData))
|
TransformerFactory.newInstance().newTransformer().transform(DOMSource(playlistXml), StreamResult(xmlData))
|
||||||
|
|
||||||
translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension).let {
|
callbackOnProgress(translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension))
|
||||||
runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
val outputFile = File.createTempFile("dash", ".mp4")
|
val outputFile = File.createTempFile("dash", ".mp4")
|
||||||
runCatching {
|
runCatching {
|
||||||
MediaDownloaderHelper.downloadDashChapterFile(
|
MediaDownloaderHelper.downloadDashChapterFile(
|
||||||
@ -264,9 +265,7 @@ class DownloadProcessor (
|
|||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
if (coroutineContext.job.isCancelled) return@onFailure
|
if (coroutineContext.job.isCancelled) return@onFailure
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
translation.format("failed_processing_toast", "error" to exception.toString()).let {
|
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
||||||
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,23 +286,24 @@ class DownloadProcessor (
|
|||||||
val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java)
|
val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java)
|
||||||
val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java)
|
val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java)
|
||||||
|
|
||||||
SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage ->
|
remoteSideContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage ->
|
||||||
translation[if (downloadStage.isFinalStage) {
|
translation[if (downloadStage.isFinalStage) {
|
||||||
"already_downloaded_toast"
|
"already_downloaded_toast"
|
||||||
} else {
|
} else {
|
||||||
"already_queued_toast"
|
"already_queued_toast"
|
||||||
}].let {
|
}].let {
|
||||||
runCatching { callback.onFailure(it, null) }.onFailure { fallbackToast(it) }
|
callbackOnFailure(it, null)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingDownloadObject = PendingDownload(
|
val pendingDownloadObject = PendingDownload(
|
||||||
metadata = downloadMetadata
|
metadata = downloadMetadata
|
||||||
)
|
).apply { downloadTaskManager = remoteSideContext.downloadTaskManager }
|
||||||
|
|
||||||
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
|
pendingDownloadObject.also {
|
||||||
pendingDownloadObject.apply {
|
remoteSideContext.downloadTaskManager.addTask(it)
|
||||||
|
}.apply {
|
||||||
job = coroutineContext.job
|
job = coroutineContext.job
|
||||||
downloadStage = DownloadStage.DOWNLOADING
|
downloadStage = DownloadStage.DOWNLOADING
|
||||||
}
|
}
|
||||||
@ -344,9 +344,7 @@ class DownloadProcessor (
|
|||||||
val renamedOverlayMedia = renameFromFileType(overlayMedia.file, overlayMedia.fileType)
|
val renamedOverlayMedia = renameFromFileType(overlayMedia.file, overlayMedia.fileType)
|
||||||
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
|
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
|
||||||
runCatching {
|
runCatching {
|
||||||
translation.format("download_toast", "path" to media.file.nameWithoutExtension).let {
|
callbackOnProgress(translation.format("download_toast", "path" to media.file.nameWithoutExtension))
|
||||||
runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.MERGING
|
pendingDownloadObject.downloadStage = DownloadStage.MERGING
|
||||||
|
|
||||||
MediaDownloaderHelper.mergeOverlayFile(
|
MediaDownloaderHelper.mergeOverlayFile(
|
||||||
@ -359,9 +357,7 @@ class DownloadProcessor (
|
|||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
if (coroutineContext.job.isCancelled) return@onFailure
|
if (coroutineContext.job.isCancelled) return@onFailure
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
translation.format("failed_processing_toast", "error" to exception.toString()).let {
|
callbackOnFailure(translation.format("failed_processing_toast", "error" to exception.toString()), exception.message)
|
||||||
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
|
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,9 +371,7 @@ class DownloadProcessor (
|
|||||||
}.onFailure { exception ->
|
}.onFailure { exception ->
|
||||||
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
pendingDownloadObject.downloadStage = DownloadStage.FAILED
|
||||||
Logger.error(exception)
|
Logger.error(exception)
|
||||||
translation["failed_generic_toast"].let {
|
callbackOnFailure(translation["failed_generic_toast"], exception.message)
|
||||||
runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,13 @@ import me.rhunk.snapenhance.SharedContextHolder
|
|||||||
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
lateinit var sections: Map<EnumSection, Section>
|
||||||
|
|
||||||
|
override fun onPostResume() {
|
||||||
|
super.onPostResume()
|
||||||
|
sections.values.forEach { it.onResumed() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -20,8 +27,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
checkForRequirements()
|
checkForRequirements()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sections = EnumSection.values().toList().associateWith {
|
sections = EnumSection.values().toList().associateWith {
|
||||||
it.section.constructors.first().call()
|
runCatching {
|
||||||
|
it.section.constructors.first().call()
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.getOrThrow()
|
||||||
}.onEach { (section, instance) ->
|
}.onEach { (section, instance) ->
|
||||||
with(instance) {
|
with(instance) {
|
||||||
enumSection = section
|
enumSection = section
|
||||||
|
@ -14,6 +14,7 @@ import androidx.navigation.compose.composable
|
|||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.HomeSection
|
import me.rhunk.snapenhance.ui.manager.sections.HomeSection
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
|
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
|
||||||
|
import me.rhunk.snapenhance.ui.manager.sections.download.DownloadSection
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -26,7 +27,8 @@ enum class EnumSection(
|
|||||||
DOWNLOADS(
|
DOWNLOADS(
|
||||||
route = "downloads",
|
route = "downloads",
|
||||||
title = "Downloads",
|
title = "Downloads",
|
||||||
icon = Icons.Filled.Download
|
icon = Icons.Filled.Download,
|
||||||
|
section = DownloadSection::class
|
||||||
),
|
),
|
||||||
FEATURES(
|
FEATURES(
|
||||||
route = "features",
|
route = "features",
|
||||||
@ -66,6 +68,7 @@ open class Section {
|
|||||||
lateinit var navController: NavController
|
lateinit var navController: NavController
|
||||||
|
|
||||||
open fun init() {}
|
open fun init() {}
|
||||||
|
open fun onResumed() {}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
open fun Content() { NotImplemented() }
|
open fun Content() { NotImplemented() }
|
||||||
|
@ -18,11 +18,13 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Section
|
||||||
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||||
import me.rhunk.snapenhance.ui.setup.Requirements
|
import me.rhunk.snapenhance.ui.setup.Requirements
|
||||||
@ -31,6 +33,7 @@ class HomeSection : Section() {
|
|||||||
companion object {
|
companion object {
|
||||||
val cardMargin = 10.dp
|
val cardMargin = 10.dp
|
||||||
}
|
}
|
||||||
|
private val installationSummary = mutableStateOf(null as InstallationSummary?)
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -72,7 +75,8 @@ class HomeSection : Section() {
|
|||||||
"Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}"
|
"Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}"
|
||||||
} else {
|
} else {
|
||||||
"Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}"
|
"Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}"
|
||||||
}, modifier = Modifier.weight(1f)
|
}, modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,6 +90,14 @@ class HomeSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResumed() {
|
||||||
|
Logger.debug("HomeSection resumed")
|
||||||
|
if (!context.mappings.isMappingsLoaded()) {
|
||||||
|
context.mappings.init()
|
||||||
|
}
|
||||||
|
installationSummary.value = context.getInstallationSummary()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
@ -105,7 +117,7 @@ class HomeSection : Section() {
|
|||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
SummaryCards(context.getInstallationSummary())
|
SummaryCards(installationSummary = installationSummary.value ?: return)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.manager.sections.download
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Section
|
||||||
|
|
||||||
|
class DownloadSection : Section() {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ object Requirements {
|
|||||||
const val LANGUAGE = 0b00010
|
const val LANGUAGE = 0b00010
|
||||||
const val MAPPINGS = 0b00100
|
const val MAPPINGS = 0b00100
|
||||||
const val SAVE_FOLDER = 0b01000
|
const val SAVE_FOLDER = 0b01000
|
||||||
const val FFMPEG = 0b10000
|
|
||||||
|
|
||||||
fun getName(requirement: Int): String {
|
fun getName(requirement: Int): String {
|
||||||
return when (requirement) {
|
return when (requirement) {
|
||||||
@ -13,7 +12,6 @@ object Requirements {
|
|||||||
LANGUAGE -> "LANGUAGE"
|
LANGUAGE -> "LANGUAGE"
|
||||||
MAPPINGS -> "MAPPINGS"
|
MAPPINGS -> "MAPPINGS"
|
||||||
SAVE_FOLDER -> "SAVE_FOLDER"
|
SAVE_FOLDER -> "SAVE_FOLDER"
|
||||||
FFMPEG -> "FFMPEG"
|
|
||||||
else -> "UNKNOWN"
|
else -> "UNKNOWN"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import me.rhunk.snapenhance.SharedContextHolder
|
import me.rhunk.snapenhance.SharedContextHolder
|
||||||
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
|
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.impl.FfmpegScreen
|
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen
|
import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen
|
import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen
|
import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen
|
||||||
@ -65,9 +64,6 @@ class SetupActivity : ComponentActivity() {
|
|||||||
if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) {
|
if (isFirstRun || hasRequirement(Requirements.MAPPINGS)) {
|
||||||
add(MappingsScreen().apply { route = "mappings" })
|
add(MappingsScreen().apply { route = "mappings" })
|
||||||
}
|
}
|
||||||
if (isFirstRun || hasRequirement(Requirements.FFMPEG)) {
|
|
||||||
add(FfmpegScreen().apply { route = "ffmpeg" })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are no required screens, we can just finish the activity
|
// If there are no required screens, we can just finish the activity
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.setup.screens.impl
|
|
||||||
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
|
|
||||||
|
|
||||||
class FfmpegScreen : SetupScreen() {
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
Text(text = "FFmpeg")
|
|
||||||
Button(onClick = { allowNext(true) }) {
|
|
||||||
Text(text = "Next")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -40,7 +40,7 @@ class PickLanguageScreen : SetupScreen(){
|
|||||||
|
|
||||||
fun getLocaleDisplayName(locale: String): String {
|
fun getLocaleDisplayName(locale: String): String {
|
||||||
locale.split("_").let {
|
locale.split("_").let {
|
||||||
return java.util.Locale(it[0], it[1]).getDisplayName(java.util.Locale.getDefault())
|
return Locale(it[0], it[1]).getDisplayName(Locale.getDefault())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,51 +18,6 @@ object SharedContext {
|
|||||||
lateinit var downloadTaskManager: DownloadTaskManager
|
lateinit var downloadTaskManager: DownloadTaskManager
|
||||||
lateinit var translation: LocaleWrapper
|
lateinit var translation: LocaleWrapper
|
||||||
|
|
||||||
private fun askForStoragePermission(context: Context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
|
||||||
intent.addCategory("android.intent.category.DEFAULT")
|
|
||||||
intent.data = android.net.Uri.parse("package:${context.packageName}")
|
|
||||||
if (context !is Activity) {
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
if (context !is Activity) {
|
|
||||||
Logger.log("Storage permission not granted, exiting")
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
context.requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun askForPermissions(context: Context) {
|
|
||||||
|
|
||||||
//ask for storage permission
|
|
||||||
val hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
Environment.isExternalStorageManager()
|
|
||||||
} else {
|
|
||||||
context.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStoragePermission) return
|
|
||||||
|
|
||||||
if (context !is Activity) {
|
|
||||||
askForStoragePermission(context)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setTitle("Storage permission")
|
|
||||||
.setMessage("App needs storage permission to download files and save them to your device. Please allow it in the next screen.")
|
|
||||||
.setPositiveButton("Grant") { _, _ ->
|
|
||||||
askForStoragePermission(context)
|
|
||||||
}
|
|
||||||
.setNegativeButton("Cancel") { _, _ ->
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureInitialized(context: Context) {
|
fun ensureInitialized(context: Context) {
|
||||||
if (!this::downloadTaskManager.isInitialized) {
|
if (!this::downloadTaskManager.isInitialized) {
|
||||||
downloadTaskManager = DownloadTaskManager().apply {
|
downloadTaskManager = DownloadTaskManager().apply {
|
||||||
|
@ -16,9 +16,11 @@ class DownloaderConfig : ConfigContainer() {
|
|||||||
"append_date_time",
|
"append_date_time",
|
||||||
"append_type",
|
"append_type",
|
||||||
"append_username"
|
"append_username"
|
||||||
)
|
).apply { set(mutableListOf("append_hash", "append_date_time", "append_type", "append_username")) }
|
||||||
val allowDuplicate = boolean("allow_duplicate")
|
val allowDuplicate = boolean("allow_duplicate")
|
||||||
val mergeOverlays = boolean("merge_overlays")
|
val mergeOverlays = boolean("merge_overlays")
|
||||||
val chatDownloadContextMenu = boolean("chat_download_context_menu")
|
val chatDownloadContextMenu = boolean("chat_download_context_menu")
|
||||||
val logging = multiple("logging", "started", "success", "progress", "failure")
|
val logging = multiple("logging", "started", "success", "progress", "failure").apply {
|
||||||
|
set(mutableListOf("started", "success"))
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ import me.rhunk.snapenhance.download.data.DownloadMetadata
|
|||||||
import me.rhunk.snapenhance.download.data.DownloadRequest
|
import me.rhunk.snapenhance.download.data.DownloadRequest
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
import me.rhunk.snapenhance.download.data.InputMedia
|
||||||
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
import me.rhunk.snapenhance.download.data.DownloadMediaType
|
||||||
|
|
||||||
class DownloadManagerClient (
|
class DownloadManagerClient (
|
||||||
private val context: ModContext,
|
private val context: ModContext,
|
||||||
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
||||||
import me.rhunk.snapenhance.download.data.PendingDownload
|
import me.rhunk.snapenhance.download.data.PendingDownload
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
import me.rhunk.snapenhance.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.ui.download.MediaFilter
|
import me.rhunk.snapenhance.ui.download.MediaFilter
|
||||||
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
|
||||||
|
|
||||||
@ -158,6 +158,7 @@ class DownloadTaskManager {
|
|||||||
iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl"))
|
iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl"))
|
||||||
)
|
)
|
||||||
).apply {
|
).apply {
|
||||||
|
downloadTaskManager = this@DownloadTaskManager
|
||||||
downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage")))
|
downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage")))
|
||||||
//if downloadStage is not saved, it means the app was killed before the download was finished
|
//if downloadStage is not saved, it means the app was killed before the download was finished
|
||||||
if (downloadStage != DownloadStage.SAVED) {
|
if (downloadStage != DownloadStage.SAVED) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.download.enums
|
package me.rhunk.snapenhance.download.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -1,7 +1,5 @@
|
|||||||
package me.rhunk.snapenhance.download.data
|
package me.rhunk.snapenhance.download.data
|
||||||
|
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
|
||||||
|
|
||||||
|
|
||||||
data class DashOptions(val offsetTime: Long, val duration: Long?)
|
data class DashOptions(val offsetTime: Long, val duration: Long?)
|
||||||
data class InputMedia(
|
data class InputMedia(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.download.enums
|
package me.rhunk.snapenhance.download.data
|
||||||
|
|
||||||
enum class DownloadStage(
|
enum class DownloadStage(
|
||||||
val isFinalStage: Boolean = false,
|
val isFinalStage: Boolean = false,
|
@ -2,15 +2,16 @@ package me.rhunk.snapenhance.download.data
|
|||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import me.rhunk.snapenhance.SharedContext
|
import me.rhunk.snapenhance.SharedContext
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||||
|
|
||||||
data class PendingDownload(
|
data class PendingDownload(
|
||||||
var downloadId: Int = 0,
|
var downloadId: Int = 0,
|
||||||
var outputFile: String? = null,
|
var outputFile: String? = null,
|
||||||
var job: Job? = null,
|
|
||||||
|
|
||||||
val metadata : DownloadMetadata
|
val metadata : DownloadMetadata
|
||||||
) {
|
) {
|
||||||
|
lateinit var downloadTaskManager: DownloadTaskManager
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
var changeListener = { _: DownloadStage, _: DownloadStage -> }
|
var changeListener = { _: DownloadStage, _: DownloadStage -> }
|
||||||
private var _stage: DownloadStage = DownloadStage.PENDING
|
private var _stage: DownloadStage = DownloadStage.PENDING
|
||||||
var downloadStage: DownloadStage
|
var downloadStage: DownloadStage
|
||||||
@ -20,15 +21,13 @@ data class PendingDownload(
|
|||||||
set(value) = synchronized(this) {
|
set(value) = synchronized(this) {
|
||||||
changeListener(_stage, value)
|
changeListener(_stage, value)
|
||||||
_stage = value
|
_stage = value
|
||||||
SharedContext.downloadTaskManager.updateTask(this)
|
downloadTaskManager.updateTask(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isJobActive(): Boolean {
|
fun isJobActive() = job?.isActive == true
|
||||||
return job?.isActive ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
job?.cancel()
|
|
||||||
downloadStage = DownloadStage.CANCELLED
|
downloadStage = DownloadStage.CANCELLED
|
||||||
|
job?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH
|
import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
@ -20,11 +19,11 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer
|
|||||||
import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap
|
import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap
|
||||||
import me.rhunk.snapenhance.database.objects.FriendInfo
|
import me.rhunk.snapenhance.database.objects.FriendInfo
|
||||||
import me.rhunk.snapenhance.download.DownloadManagerClient
|
import me.rhunk.snapenhance.download.DownloadManagerClient
|
||||||
|
import me.rhunk.snapenhance.download.data.DownloadMediaType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
import me.rhunk.snapenhance.download.data.DownloadMetadata
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
import me.rhunk.snapenhance.download.data.InputMedia
|
||||||
import me.rhunk.snapenhance.download.data.SplitMediaAssetType
|
import me.rhunk.snapenhance.download.data.SplitMediaAssetType
|
||||||
import me.rhunk.snapenhance.download.data.toKeyPair
|
import me.rhunk.snapenhance.download.data.toKeyPair
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
|
||||||
import me.rhunk.snapenhance.features.Feature
|
import me.rhunk.snapenhance.features.Feature
|
||||||
import me.rhunk.snapenhance.features.FeatureLoadParams
|
import me.rhunk.snapenhance.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.features.impl.Messaging
|
import me.rhunk.snapenhance.features.impl.Messaging
|
||||||
@ -52,11 +51,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||||
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
|
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
|
||||||
private var lastSeenMapParams: ParamMap? = null
|
private var lastSeenMapParams: ParamMap? = null
|
||||||
private val isFFmpegPresent by lazy {
|
|
||||||
runCatching { FFmpegKit.execute("-version") }.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun provideClientDownloadManager(
|
private fun provideDownloadManagerClient(
|
||||||
pathSuffix: String,
|
pathSuffix: String,
|
||||||
mediaIdentifier: String,
|
mediaIdentifier: String,
|
||||||
mediaDisplaySource: String? = null,
|
mediaDisplaySource: String? = null,
|
||||||
@ -115,10 +111,6 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canMergeOverlay(): Boolean {
|
|
||||||
if (!context.config.downloader.autoDownloadOptions.get().contains("merge_overlay")) return false
|
|
||||||
return isFFmpegPresent
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: implement subfolder argument
|
//TODO: implement subfolder argument
|
||||||
private fun createNewFilePath(hexHash: String, mediaDisplayType: String?, pathPrefix: String): String {
|
private fun createNewFilePath(hexHash: String, mediaDisplayType: String?, pathPrefix: String): String {
|
||||||
@ -246,7 +238,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
val author = context.database.getFriendInfo(senderId) ?: return
|
val author = context.database.getFriendInfo(senderId) ?: return
|
||||||
val authorUsername = author.usernameForSorting!!
|
val authorUsername = author.usernameForSorting!!
|
||||||
|
|
||||||
downloadOperaMedia(provideClientDownloadManager(
|
downloadOperaMedia(provideDownloadManagerClient(
|
||||||
pathSuffix = authorUsername,
|
pathSuffix = authorUsername,
|
||||||
mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}",
|
mediaIdentifier = "$conversationId$senderId${conversationMessage.server_message_id}",
|
||||||
mediaDisplaySource = authorUsername,
|
mediaDisplaySource = authorUsername,
|
||||||
@ -286,7 +278,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
) ?: throw Exception("Friend not found in database")
|
) ?: throw Exception("Friend not found in database")
|
||||||
val authorName = author.usernameForSorting!!
|
val authorName = author.usernameForSorting!!
|
||||||
|
|
||||||
downloadOperaMedia(provideClientDownloadManager(
|
downloadOperaMedia(provideDownloadManagerClient(
|
||||||
pathSuffix = authorName,
|
pathSuffix = authorName,
|
||||||
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
|
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
|
||||||
mediaDisplaySource = authorName,
|
mediaDisplaySource = authorName,
|
||||||
@ -305,7 +297,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
"[^\\x00-\\x7F]".toRegex(),
|
"[^\\x00-\\x7F]".toRegex(),
|
||||||
"")
|
"")
|
||||||
|
|
||||||
downloadOperaMedia(provideClientDownloadManager(
|
downloadOperaMedia(provideDownloadManagerClient(
|
||||||
pathSuffix = "Public-Stories/$userDisplayName",
|
pathSuffix = "Public-Stories/$userDisplayName",
|
||||||
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
||||||
mediaDisplayType = userDisplayName,
|
mediaDisplayType = userDisplayName,
|
||||||
@ -316,7 +308,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
|
|
||||||
//spotlight
|
//spotlight
|
||||||
if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) {
|
if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) {
|
||||||
downloadOperaMedia(provideClientDownloadManager(
|
downloadOperaMedia(provideDownloadManagerClient(
|
||||||
pathSuffix = "Spotlight",
|
pathSuffix = "Spotlight",
|
||||||
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
mediaIdentifier = paramMap["SNAP_ID"].toString(),
|
||||||
mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType,
|
mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType,
|
||||||
@ -328,11 +320,6 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
//stories with mpeg dash media
|
//stories with mpeg dash media
|
||||||
//TODO: option to download multiple chapters
|
//TODO: option to download multiple chapters
|
||||||
if (paramMap.containsKey("LONGFORM_VIDEO_PLAYLIST_ITEM") && forceDownload) {
|
if (paramMap.containsKey("LONGFORM_VIDEO_PLAYLIST_ITEM") && forceDownload) {
|
||||||
if (!isFFmpegPresent) {
|
|
||||||
context.shortToast("Can't download media. ffmpeg was not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val storyName = paramMap["STORY_NAME"].toString().replace(
|
val storyName = paramMap["STORY_NAME"].toString().replace(
|
||||||
"[^\\x00-\\x7F]".toRegex(),
|
"[^\\x00-\\x7F]".toRegex(),
|
||||||
"")
|
"")
|
||||||
@ -361,7 +348,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provideClientDownloadManager(
|
provideDownloadManagerClient(
|
||||||
pathSuffix = "Pro-Stories/${storyName}",
|
pathSuffix = "Pro-Stories/${storyName}",
|
||||||
mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}",
|
mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}",
|
||||||
mediaDisplaySource = storyName,
|
mediaDisplaySource = storyName,
|
||||||
@ -396,10 +383,12 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
|
|
||||||
val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>()
|
val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>()
|
||||||
val isVideo = mediaParamMap.containsKey("video_media_info_list")
|
val isVideo = mediaParamMap.containsKey("video_media_info_list")
|
||||||
|
val canMergeOverlay = context.config.downloader.autoDownloadOptions.get().contains("merge_overlay")
|
||||||
|
|
||||||
mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo(
|
mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo(
|
||||||
(if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
|
(if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
|
||||||
)
|
)
|
||||||
if (canMergeOverlay() && mediaParamMap.containsKey("overlay_image_media_info")) {
|
if (canMergeOverlay && mediaParamMap.containsKey("overlay_image_media_info")) {
|
||||||
mediaInfoMap[SplitMediaAssetType.OVERLAY] =
|
mediaInfoMap[SplitMediaAssetType.OVERLAY] =
|
||||||
MediaInfo(mediaParamMap["overlay_image_media_info"]!!)
|
MediaInfo(mediaParamMap["overlay_image_media_info"]!!)
|
||||||
}
|
}
|
||||||
@ -483,7 +472,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
runCatching {
|
runCatching {
|
||||||
if (!isPreview) {
|
if (!isPreview) {
|
||||||
val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage)
|
val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage)
|
||||||
provideClientDownloadManager(
|
provideDownloadManagerClient(
|
||||||
pathSuffix = authorName,
|
pathSuffix = authorName,
|
||||||
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}",
|
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.server_message_id}",
|
||||||
mediaDisplaySource = authorName,
|
mediaDisplaySource = authorName,
|
||||||
|
@ -27,7 +27,7 @@ import me.rhunk.snapenhance.SharedContext
|
|||||||
import me.rhunk.snapenhance.core.R
|
import me.rhunk.snapenhance.core.R
|
||||||
import me.rhunk.snapenhance.data.FileType
|
import me.rhunk.snapenhance.data.FileType
|
||||||
import me.rhunk.snapenhance.download.data.PendingDownload
|
import me.rhunk.snapenhance.download.data.PendingDownload
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
import me.rhunk.snapenhance.download.data.DownloadStage
|
||||||
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
import me.rhunk.snapenhance.util.snap.PreviewUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
Loading…
x
Reference in New Issue
Block a user