commit 0ba1eb4a8b067c8c8535409b3d432c912dc2e1a3
parent e4443279d67e61d6f858324a6d745cfb3608bd04
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sun, 8 Oct 2023 18:41:18 +0200
feat: bypass video length restriction
- single and split mode
Diffstat:
6 files changed, 95 insertions(+), 56 deletions(-)
diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json
@@ -362,9 +362,9 @@
"name": "Block Ads",
"description": "Prevents Advertisements from being displayed"
},
- "disable_video_length_restrictions": {
- "name": "Disable Video Length Restrictions",
- "description": "Disables Snapchat's maximum video length restriction"
+ "bypass_video_length_restriction": {
+ "name": "Bypass Video Length Restrictions",
+ "description": "Single: sends a single video\nSplit: split videos after editing"
},
"disable_google_play_dialogs": {
"name": "Disable Google Play Services Dialogs",
@@ -664,6 +664,10 @@
"added_by_group_chat": "By Group Chat",
"added_by_qr_code": "By QR Code",
"added_by_community": "By Community"
+ },
+ "bypass_video_length_restriction": {
+ "single": "Single media",
+ "split": "Split media"
}
}
},
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt
@@ -7,7 +7,7 @@ class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads")
- val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.BAN_RISK) }
+ val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") { requireRestart() }
val forceMediaSourceQuality = boolean("force_media_source_quality")
val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR) }
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/BypassVideoLengthRestriction.kt
@@ -0,0 +1,72 @@
+package me.rhunk.snapenhance.features.impl.tweaks
+
+import android.os.Build
+import android.os.FileObserver
+import com.google.gson.JsonParser
+import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
+import me.rhunk.snapenhance.core.util.ktx.setObjectField
+import me.rhunk.snapenhance.features.Feature
+import me.rhunk.snapenhance.features.FeatureLoadParams
+import me.rhunk.snapenhance.hook.HookStage
+import me.rhunk.snapenhance.hook.hookConstructor
+import java.io.File
+
+class BypassVideoLengthRestriction :
+ Feature("BypassVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
+ private lateinit var fileObserver: FileObserver
+
+ override fun asyncOnActivityCreate() {
+ val mode = context.config.global.bypassVideoLengthRestriction.getNullable()
+
+ if (mode == "single") {
+ //fix black videos when story is posted
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val postedStorySnapFolder =
+ File(context.androidContext.filesDir, "file_manager/posted_story_snap")
+
+ fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) {
+ override fun onEvent(event: Int, path: String?) {
+ if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return
+ fileObserver.stopWatching()
+
+ val file = File(postedStorySnapFolder, path)
+ runCatching {
+ val fileContent = JsonParser.parseReader(file.reader()).asJsonObject
+ if (fileContent["timerOrDuration"].asLong < 0) file.delete()
+ }.onFailure {
+ context.log.error("Failed to read story metadata file", it)
+ }
+ }
+ })
+
+ context.event.subscribe(SendMessageWithContentEvent::class) { event ->
+ if (event.destinations.stories.isEmpty()) return@subscribe
+ fileObserver.startWatching()
+ }
+ }
+
+ context.mappings.getMappedClass("DefaultMediaItem")
+ .hookConstructor(HookStage.BEFORE) { param ->
+ //set the video length argument
+ param.setArg(5, -1L)
+ }
+ }
+
+ //TODO: allow split from any source
+ if (mode == "split") {
+ val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId")
+ // memories grid
+ findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param ->
+ //set the durationMs field
+ param.thisObject<Any>()
+ .setObjectField(cameraRollId["durationMsField"].toString(), -1L)
+ }
+
+ // chat camera roll grid
+ findClass("com.snap.impala.common.media.MediaLibraryItem").hookConstructor(HookStage.BEFORE) { param ->
+ //set the video length argument
+ param.setArg(3, -1L)
+ }
+ }
+ }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt
@@ -1,50 +0,0 @@
-package me.rhunk.snapenhance.features.impl.tweaks
-
-import android.os.Build
-import android.os.FileObserver
-import com.google.gson.JsonParser
-import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
-import me.rhunk.snapenhance.features.Feature
-import me.rhunk.snapenhance.features.FeatureLoadParams
-import me.rhunk.snapenhance.hook.HookStage
-import me.rhunk.snapenhance.hook.Hooker
-import java.io.File
-
-class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
- private lateinit var fileObserver: FileObserver
-
- override fun asyncOnActivityCreate() {
- val defaultMediaItem = context.mappings.getMappedClass("DefaultMediaItem")
- val isState by context.config.global.disableVideoLengthRestrictions
-
- //fix black videos when story is posted
- if (isState && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- val postedStorySnapFolder = File(context.androidContext.filesDir, "file_manager/posted_story_snap")
-
- fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) {
- override fun onEvent(event: Int, path: String?) {
- if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return
- fileObserver.stopWatching()
-
- val file = File(postedStorySnapFolder, path)
- runCatching {
- val fileContent = JsonParser.parseReader(file.reader()).asJsonObject
- if (fileContent["timerOrDuration"].asLong < 0) file.delete()
- }.onFailure {
- context.log.error("Failed to read story metadata file", it)
- }
- }
- })
-
- context.event.subscribe(SendMessageWithContentEvent::class) { event ->
- if (event.destinations.stories.isEmpty()) return@subscribe
- fileObserver.startWatching()
- }
- }
-
- Hooker.hookConstructor(defaultMediaItem, HookStage.BEFORE, { isState }) { param ->
- //set the video length argument
- param.setArg(5, -1L)
- }
- }
-}-
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt
@@ -84,7 +84,7 @@ class FeatureManager(
ConfigurationOverride::class,
SendOverride::class,
UnlimitedSnapViewTime::class,
- DisableVideoLengthRestriction::class,
+ BypassVideoLengthRestriction::class,
MediaQualityLevelOverride::class,
MeoPasscodeBypass::class,
AppPasscode::class,
diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt
@@ -1,6 +1,7 @@
package me.rhunk.snapenhance.mapper.impl
import me.rhunk.snapenhance.mapper.AbstractClassMapper
+import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.isAbstract
@@ -8,6 +9,18 @@ class DefaultMediaItemMapper : AbstractClassMapper() {
init {
mapper {
for (clazz in classes) {
+ if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) {
+ continue
+ }
+ val durationMsField = clazz.fields.firstOrNull { it.type == "J" } ?: continue
+
+ addMapping("CameraRollMediaId", "class" to clazz.getClassName(), "durationMsField" to durationMsField.name)
+ return@mapper
+ }
+ }
+
+ mapper {
+ for (clazz in classes) {
val superClass = getClass(clazz.superclass) ?: continue
if (!superClass.isAbstract() || superClass.interfaces.isEmpty() || superClass.interfaces[0] != "Ljava/lang/Comparable;") continue