feat: sponsorblock patch (#101)

* chore(release): 2.5.1-dev.1 [skip ci]

## [2.5.1-dev.1](https://github.com/revanced/revanced-patches/compare/v2.5.0...v2.5.1-dev.1) (2022-06-30)

### Bug Fixes

* freezing panels when watching video in fullscreen ([#89](https://github.com/revanced/revanced-patches/issues/89)) ([f5d4f6c](f5d4f6c341))
* invalid version in compatibility annotation ([#90](https://github.com/revanced/revanced-patches/issues/90)) ([df43547](df435475cd))

* feat: migrate to breaking changes of patcher

* chore(release): 2.6.0-dev.1 [skip ci]

# [2.6.0-dev.1](https://github.com/revanced/revanced-patches/compare/v2.5.1-dev.1...v2.6.0-dev.1) (2022-07-02)

### Features

* migrate to breaking changes of patcher ([a116852](a11685263f))

* refactor: add package for fingerprints

* feat: partial `sponsorblock` patch

* feat: add `Name` annotation to `sponsorblock-resource-patch`

* fix: `sponsorblock-resource-patch`

* refactor: remove unused resources

* refactor: remove `locale-config-fix` dependency

* fix: broken fingerprints

* feat: general fixes on `sponsorblock` patch

* feat: more fixes on `sponsorblock` patch

* feat: update `sponsorblock` patch to 17.26.35

* style: use better wording and fix spelling mistakes

* fix: finish `sponsorblock` patch

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
This commit is contained in:
oSumAtrIX 2022-07-18 01:17:03 +02:00 committed by GitHub
parent 8f06ea9115
commit 36af4cc14f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1149 additions and 7 deletions

View File

@ -0,0 +1,13 @@
package app.revanced.patches.youtube.layout.sponsorblock.annotations
import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Package
@Compatibility(
[Package(
"com.google.android.youtube", arrayOf("17.22.36", "17.26.35", "17.27.39")
)]
)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
internal annotation class SponsorBlockCompatibility

View File

@ -0,0 +1,41 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
@Name("append-time-fingerprint")
@MatchingMethod(
"Liet;", "e"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object AppendTimeFingerprint : MethodFingerprint(
"V",
AccessFlags.PUBLIC or AccessFlags.FINAL,
listOf("L", "L", "L"),
listOf(
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.IGET_OBJECT,
Opcode.IGET_OBJECT,
Opcode.CHECK_CAST,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT,
Opcode.IF_NEZ,
Opcode.IGET_OBJECT,
Opcode.IGET_OBJECT,
Opcode.CHECK_CAST,
Opcode.INVOKE_VIRTUAL,
Opcode.RETURN_VOID
)
)

View File

@ -0,0 +1,21 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("create-video-player-seekbar-fingerprint")
@MatchingMethod(
"Lfcm;", "onDraw"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object CreateVideoPlayerSeekbarFingerprint : MethodFingerprint(
"V", null, null,
null,
listOf("timed_markers_width")
)

View File

@ -0,0 +1,24 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import org.jf.dexlib2.util.MethodUtil
@Name("next-gen-watch-layout-fingerprint")
@MatchingMethod(
"LNextGenWatchLayout;", "<init>"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object NextGenWatchLayoutFingerprint : MethodFingerprint(
"V", // constructors return void, in favour of speed of matching, this fingerprint has been added
null,
null,
null,
customFingerprint = { methodDef -> MethodUtil.isConstructor(methodDef) && methodDef.parameterTypes.size == 3 && methodDef.definingClass.endsWith("NextGenWatchLayout;") }
)

View File

@ -0,0 +1,22 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import org.jf.dexlib2.Opcode
@Name("player-controller-set-time-reference-fingerprint")
@MatchingMethod(
"Lxqm;", "<init>"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object PlayerControllerSetTimeReferenceFingerprint : MethodFingerprint(
null, null, null,
listOf(Opcode.INVOKE_DIRECT_RANGE, Opcode.IGET_OBJECT),
listOf("Media progress reported outside media playback: ")
)

View File

@ -0,0 +1,23 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("player-init-fingerprint")
@MatchingMethod(
"Laajv;", "aG"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object PlayerInitFingerprint : MethodFingerprint(
null, null, null,
null,
strings = listOf(
"playVideo called on player response with no videoStreamingData."
),
)

View File

@ -0,0 +1,22 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("player-overlays-layout-init-fingerprint")
@MatchingMethod(
"Lihh;", "u"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object PlayerOverlaysLayoutInitFingerprint : MethodFingerprint(
null, null, null,
null,
null,
{ methodDef -> methodDef.returnType.endsWith("YouTubePlayerOverlaysLayout;") }
)

View File

@ -0,0 +1,37 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.MethodReference
@Name("rectangle-field-invalidator-fingerprint")
@MatchingMethod(
"Lfcm;", "kY"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object RectangleFieldInvalidatorFingerprint : MethodFingerprint(
"V",
null,
null,
null,
null,
custom@{ methodDef ->
val instructions = methodDef.implementation?.instructions!!
val instructionCount = instructions.count()
// the method has definitely more than 5 instructions
if (instructionCount < 5) return@custom false
val referenceInstruction = instructions.elementAt(instructionCount - 2) // the second to last instruction
val reference = ((referenceInstruction as? ReferenceInstruction)?.reference as? MethodReference)
reference?.parameterTypes?.size == 1 && reference.name == "invalidate" // the reference is the invalidate(..) method
}
)

View File

@ -0,0 +1,23 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("seek-fingerprint")
@MatchingMethod(
"Laajv;", "af"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object SeekFingerprint : MethodFingerprint(
null,
null,
null,
null,
listOf("Attempting to seek during an ad")
)

View File

@ -0,0 +1,19 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("show-player-controls-fingerprint")
@MatchingMethod(
"LYouTubeControlsOverlay;", "ac"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object ShowPlayerControlsFingerprint : MethodFingerprint(
"V", null, listOf("Z","Z"), null, null
)

View File

@ -0,0 +1,34 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import org.jf.dexlib2.Opcode
@Name("video-length-fingerprint")
@MatchingMethod(
"Lyfh;", "z"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object VideoLengthFingerprint : MethodFingerprint(
null, null, null,
listOf(
Opcode.MOVE_RESULT_WIDE,
Opcode.CMP_LONG,
Opcode.IF_LEZ,
Opcode.IGET_OBJECT,
Opcode.CHECK_CAST,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_WIDE,
Opcode.GOTO,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_WIDE,
Opcode.CONST_4,
Opcode.INVOKE_VIRTUAL
)
)

View File

@ -0,0 +1,20 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
@Name("video-time-fingerprint")
@MatchingMethod(
"Lwyh;", "<init>"
)
@DirectPatternScanMethod
@SponsorBlockCompatibility
@Version("0.0.1")
object VideoTimeFingerprint : MethodFingerprint(
null, null, null, null,
listOf("MedialibPlayerTimeInfo{currentPositionMillis=")
)

View File

@ -0,0 +1,335 @@
package app.revanced.patches.youtube.layout.sponsorblock.bytecode.patch
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.data.impl.toMethodWalker
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.or
import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.Dependencies
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.impl.BytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints.*
import app.revanced.patches.youtube.layout.sponsorblock.resource.patch.SponsorBlockResourcePatch
import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch
import app.revanced.patches.youtube.misc.mapping.patch.ResourceIdMappingProviderResourcePatch
import app.revanced.patches.youtube.misc.videoid.patch.VideoIdPatch
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.builder.MutableMethodImplementation
import org.jf.dexlib2.iface.instruction.*
import org.jf.dexlib2.iface.instruction.formats.Instruction35c
import org.jf.dexlib2.iface.reference.FieldReference
import org.jf.dexlib2.iface.reference.MethodReference
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.immutable.ImmutableMethod
import org.jf.dexlib2.immutable.ImmutableMethodParameter
import org.jf.dexlib2.util.MethodUtil
@Patch
@Dependencies(
dependencies = [IntegrationsPatch::class, ResourceIdMappingProviderResourcePatch::class, SponsorBlockResourcePatch::class, VideoIdPatch::class]
)
@Name("sponsorblock")
@Description("Integrate SponsorBlock.")
@SponsorBlockCompatibility
@Version("0.0.1")
class SponsorBlockBytecodePatch : BytecodePatch(
listOf(
PlayerControllerSetTimeReferenceFingerprint,
CreateVideoPlayerSeekbarFingerprint,
VideoTimeFingerprint,
NextGenWatchLayoutFingerprint,
AppendTimeFingerprint,
PlayerInitFingerprint,
PlayerOverlaysLayoutInitFingerprint
)
) {
override fun execute(data: BytecodeData): PatchResult {/*
Set current video time
*/
val referenceResult = PlayerControllerSetTimeReferenceFingerprint.result!!
val playerControllerSetTimeMethod =
data.toMethodWalker(referenceResult.method).nextMethod(referenceResult.patternScanResult!!.startIndex, true)
.getMethod() as MutableMethod
playerControllerSetTimeMethod.addInstruction(
2,
"invoke-static {p1, p2}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoTime(J)V"
)
/*
Set current video time high precision
*/
val constructorFingerprint =
object : MethodFingerprint("V", null, listOf("J", "J", "J", "J", "I", "L"), null) {}
constructorFingerprint.resolve(data, VideoTimeFingerprint.result!!.classDef)
val constructor = constructorFingerprint.result!!.mutableMethod
constructor.addInstruction(
0,
"invoke-static {p1, p2}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoTimeHighPrecision(J)V"
)
/*
Set current video id
*/
VideoIdPatch.injectCall("Lapp/revanced/integrations/sponsorblock/PlayerController;->setCurrentVideoId(Ljava/lang/String;)V")
/*
Seekbar drawing
*/
val seekbarSignatureResult = CreateVideoPlayerSeekbarFingerprint.result!!
val seekbarMethod = seekbarSignatureResult.mutableMethod
val seekbarMethodInstructions = seekbarMethod.implementation!!.instructions
/*
Get the instance of the seekbar rectangle
*/
seekbarMethod.addInstruction(
1,
"invoke-static {v0}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarRect(Ljava/lang/Object;)V"
)
for ((index, instruction) in seekbarMethodInstructions.withIndex()) {
if (instruction.opcode != Opcode.INVOKE_STATIC) continue
val invokeInstruction = instruction as Instruction35c
if ((invokeInstruction.reference as MethodReference).name != "round") continue
val insertIndex = index + 2
// set the thickness of the segment
seekbarMethod.addInstruction(
insertIndex,
"invoke-static {v${invokeInstruction.registerC}}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarThickness(I)V"
)
break
}
/*
Set rectangle absolute left and right positions
*/
val drawRectangleInstructions = seekbarMethodInstructions.filter {
it is ReferenceInstruction && (it.reference as? MethodReference)?.name == "drawRect" && it is FiveRegisterInstruction
}.map { // TODO: improve code
seekbarMethodInstructions.indexOf(it) to (it as FiveRegisterInstruction).registerD
}
val (indexRight, rectangleRightRegister) = drawRectangleInstructions[0]
val (indexLeft, rectangleLeftRegister) = drawRectangleInstructions[3]
// order of operation is important here due to the code above which has to be improved
// the reason for that is that we get the index, add instructions and then the offset would be wrong
seekbarMethod.addInstruction(
indexLeft + 1,
"invoke-static {v$rectangleLeftRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarAbsoluteLeft(Landroid/graphics/Rect;)V"
)
seekbarMethod.addInstruction(
indexRight + 1,
"invoke-static {v$rectangleRightRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setSponsorBarAbsoluteRight(Landroid/graphics/Rect;)V"
)
/*
Draw segment
*/
val drawSegmentInstructionInsertIndex = (seekbarMethodInstructions.size - 1 - 2)
val (canvasInstance, centerY) = (seekbarMethodInstructions[drawSegmentInstructionInsertIndex] as FiveRegisterInstruction).let {
it.registerC to it.registerE
}
seekbarMethod.addInstruction(
drawSegmentInstructionInsertIndex - 1,
"invoke-static {v$canvasInstance, v$centerY}, Lapp/revanced/integrations/sponsorblock/PlayerController;->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V"
)
/*
Set video length
*/
VideoLengthFingerprint.resolve(data, seekbarSignatureResult.classDef)
val videoLengthMethodResult = VideoLengthFingerprint.result!!
val videoLengthMethod = videoLengthMethodResult.mutableMethod
val videoLengthMethodInstructions = videoLengthMethod.implementation!!.instructions
val videoLengthRegister =
(videoLengthMethodInstructions[videoLengthMethodResult.patternScanResult!!.endIndex - 2] as OneRegisterInstruction).registerA
val dummyRegisterForLong =
videoLengthRegister + 1 // this is required for long values since they are 64 bit wide
videoLengthMethod.addInstruction(
videoLengthMethodResult.patternScanResult!!.endIndex,
"invoke-static {v$videoLengthRegister, v$dummyRegisterForLong}, Lapp/revanced/integrations/sponsorblock/PlayerController;->setVideoLength(J)V"
)
/*
Voting & Shield button
*/
ShowPlayerControlsFingerprint.resolve(data, data.classes.find { it.type.endsWith("YouTubeControlsOverlay;") }!!)
val controlsMethodResult = ShowPlayerControlsFingerprint.result!!
val controlsLayoutStubResourceId =
ResourceIdMappingProviderResourcePatch.resourceMappings.single { it.type == "id" && it.name == "controls_layout_stub" }.id
val zoomOverlayResourceId =
ResourceIdMappingProviderResourcePatch.resourceMappings.single { it.type == "id" && it.name == "video_zoom_overlay_stub" }.id
methods@ for (method in controlsMethodResult.mutableClass.methods) {
val instructions = method.implementation?.instructions!!
instructions@ for ((index, instruction) in instructions.withIndex()) {
// search for method which inflates the controls layout view
if (instruction.opcode != Opcode.CONST) continue@instructions
when ((instruction as NarrowLiteralInstruction).wideLiteral) {
controlsLayoutStubResourceId -> {
// replace the view with the YouTubeControlsOverlay
val moveResultInstructionIndex = index + 5
val inflatedViewRegister =
(instructions[moveResultInstructionIndex] as OneRegisterInstruction).registerA
// initialize with the player overlay object
method.addInstructions(
moveResultInstructionIndex + 1, // insert right after moving the view to the register and use that register
"""
invoke-static {v$inflatedViewRegister}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->initialize(Ljava/lang/Object;)V
invoke-static {v$inflatedViewRegister}, Lapp/revanced/integrations/sponsorblock/VotingButton;->initialize(Ljava/lang/Object;)V
"""
)
}
zoomOverlayResourceId -> {
val invertVisibilityMethod =
data.toMethodWalker(method).nextMethod(index - 6, true).getMethod() as MutableMethod
// change visibility of the buttons
invertVisibilityMethod.addInstructions(
0, """
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibilityNegatedImmediate(Z)V
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibilityNegatedImmediate(Z)V
""".trimIndent()
)
}
}
}
}
// change visibility of the buttons
controlsMethodResult.mutableMethod.addInstructions(
0, """
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibility(Z)V
invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibility(Z)V
""".trimIndent()
)
// set SegmentHelperLayout.context to the player layout instance
val instanceRegister = 0
NextGenWatchLayoutFingerprint.result!!.mutableMethod.addInstruction(
3, // after super call
"invoke-static/range {p$instanceRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->addSkipSponsorView15(Landroid/view/View;)V"
)
// append the new time to the player layout
val appendTimeFingerprintResult = AppendTimeFingerprint.result!!
val appendTimePatternScanStartIndex = appendTimeFingerprintResult.patternScanResult!!.startIndex
val targetRegister =
(appendTimeFingerprintResult.method.implementation!!.instructions.elementAt(appendTimePatternScanStartIndex + 1) as OneRegisterInstruction).registerA
appendTimeFingerprintResult.mutableMethod.addInstructions(
appendTimePatternScanStartIndex + 2, """
invoke-static {v$targetRegister}, Lapp/revanced/integrations/sponsorblock/SponsorBlockUtils;->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$targetRegister
"""
)
// initialize the player controller
val initFingerprintResult = PlayerInitFingerprint.result!!
val initInstanceRegister = 0
initFingerprintResult.mutableClass.methods.first { MethodUtil.isConstructor(it) }.addInstruction(
4, // after super class invoke
"invoke-static {v$initInstanceRegister}, Lapp/revanced/integrations/sponsorblock/PlayerController;->onCreate(Ljava/lang/Object;)V"
)
// initialize the sponsorblock view
PlayerOverlaysLayoutInitFingerprint.result!!.mutableMethod.addInstruction(
6, // after inflating the view
"invoke-static {p0}, Lapp/revanced/integrations/sponsorblock/player/ui/SponsorBlockView;->initialize(Ljava/lang/Object;)V"
)
// lastly create hooks for the player controller
// get original seek method
SeekFingerprint.resolve(data, initFingerprintResult.classDef)
val seekFingerprintResultMethod = SeekFingerprint.result!!.method
// get enum type for the seek helper method
val seekSourceEnumType = seekFingerprintResultMethod.parameterTypes[1].toString()
// create helper method
val seekHelperMethod = ImmutableMethod(
seekFingerprintResultMethod.definingClass,
"seekHelper",
listOf(ImmutableMethodParameter("J", null, "time")),
"Z",
AccessFlags.PUBLIC or AccessFlags.FINAL,
null, null,
MutableMethodImplementation(4)
).toMutable()
// insert helper method instructions
seekHelperMethod.addInstructions(
0,
"""
sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType
invoke-virtual {p0, p1, p2, v0}, ${seekFingerprintResultMethod.definingClass}->${seekFingerprintResultMethod.name}(J$seekSourceEnumType)Z
move-result p1
return p1
"""
)
// add the helper method to the original class
initFingerprintResult.mutableClass.methods.add(seekHelperMethod)
// get rectangle field name
RectangleFieldInvalidatorFingerprint.resolve(data, seekbarSignatureResult.classDef)
val rectangleFieldInvalidatorInstructions =
RectangleFieldInvalidatorFingerprint.result!!.method.implementation!!.instructions
val rectangleFieldName =
((rectangleFieldInvalidatorInstructions.elementAt(rectangleFieldInvalidatorInstructions.count() - 3) as ReferenceInstruction).reference as FieldReference).name
// get the player controller class from the integrations
val playerControllerMethods =
data.proxy(data.classes.first { it.type.endsWith("PlayerController;") }).resolve().methods
// get the method which contain the "replaceMe" strings
val replaceMeMethods =
playerControllerMethods.filter { it.name == "onCreate" || it.name == "setSponsorBarRect" }
fun MutableMethod.replaceStringInstruction(index: Int, instruction: Instruction, with: String) {
val register = (instruction as OneRegisterInstruction).registerA
this.replaceInstruction(
index, "const-string v$register, \"$with\""
)
}
// replace the "replaceMeWith*" strings
for (method in replaceMeMethods) {
for ((index, it) in method.implementation!!.instructions.withIndex()) {
if (it.opcode.ordinal != Opcode.CONST_STRING.ordinal) continue
when (((it as ReferenceInstruction).reference as StringReference).string) {
"replaceMeWithsetSponsorBarRect" ->
method.replaceStringInstruction(index, it, rectangleFieldName)
"replaceMeWithsetMillisecondMethod" ->
method.replaceStringInstruction(index, it, "seekHelper")
}
}
}
// TODO: isSBChannelWhitelisting implementation
return PatchResultSuccess()
}
}

View File

@ -0,0 +1,128 @@
package app.revanced.patches.youtube.layout.sponsorblock.resource.patch
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.DomFileEditor
import app.revanced.patcher.data.impl.ResourceData
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.Dependencies
import app.revanced.patcher.patch.impl.ResourcePatch
import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatch
import java.io.OutputStream
import java.nio.file.Files
@Name("sponsorblock-resource-patch")
@SponsorBlockCompatibility
@Dependencies([FixLocaleConfigErrorPatch::class])
@Version("0.0.1")
class SponsorBlockResourcePatch : ResourcePatch() {
override fun execute(data: ResourceData): PatchResult {
val classLoader = this.javaClass.classLoader
/*
merge SponsorBlock strings to main strings
*/
val stringsResourcePath = "values/strings.xml"
val stringsResourceInputStream = classLoader.getResourceAsStream("sponsorblock/$stringsResourcePath")!!
// copy nodes from the resources node to the real resource node
"resources".copyXmlNode(
data.xmlEditor[stringsResourceInputStream, OutputStream.nullOutputStream()],
data.xmlEditor["res/$stringsResourcePath"]
).close() // close afterwards
/*
merge SponsorBlock drawables to main drawables
*/
val drawables = "drawable" to arrayOf(
"ic_sb_adjust",
"ic_sb_compare",
"ic_sb_edit",
"ic_sb_logo",
"ic_sb_publish",
"ic_sb_voting"
)
val layouts = "layout" to arrayOf(
"inline_sponsor_overlay", "new_segment", "skip_sponsor_button"
)
// collect resources
val xmlResources = arrayOf(drawables, layouts)
// write resources
xmlResources.forEach { (path, resourceNames) ->
resourceNames.forEach { name ->
val relativePath = "$path/$name.xml"
Files.copy(
classLoader.getResourceAsStream("sponsorblock/$relativePath")!!,
data["res"].resolve(relativePath).toPath()
)
}
}
/*
merge xml nodes from the host to their real xml files
*/
// collect all host resources
val hostingXmlResources = mapOf("layout" to arrayOf("youtube_controls_layout"))
// copy nodes from host resources to their real xml files
hostingXmlResources.forEach { (path, resources) ->
resources.forEach { resource ->
val hostingResourceStream = classLoader.getResourceAsStream("sponsorblock/host/$path/$resource.xml")!!
val targetXmlEditor = data.xmlEditor["res/$path/$resource.xml"]
"RelativeLayout".copyXmlNode(
data.xmlEditor[hostingResourceStream, OutputStream.nullOutputStream()],
targetXmlEditor
).also {
val children = targetXmlEditor.file.getElementsByTagName("RelativeLayout").item(0).childNodes
// Replace the startOf with the voting button view so that the button does not overlap
for (i in 1 until children.length) {
val view = children.item(i)
// Replace the attribute for a specific node only
if (!view.attributes.getNamedItem("android:id").nodeValue.endsWith("live_chat_overlay_button")) continue
// voting button id from the voting button view from the youtube_controls_layout.xml host file
val votingButtonId = "@+id/voting_button"
view.attributes.getNamedItem("android:layout_toStartOf").nodeValue = votingButtonId
break
}
}.close() // close afterwards
}
}
return PatchResultSuccess()
}
/**
* Copies the specified node of the source [DomFileEditor] to the target [DomFileEditor].
* @param source the source [DomFileEditor].
* @param target the target [DomFileEditor]-
*/
private fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable {
val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes
val destinationResourceFile = target.file
val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0)
for (index in 0 until hostNodes.length) {
val node = hostNodes.item(index).cloneNode(true)
destinationResourceFile.adoptNode(node)
destinationNode.appendChild(node)
}
return AutoCloseable {
source.close()
target.close()
}
}
}

View File

@ -5,14 +5,16 @@ import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.Dependencies
import app.revanced.patcher.patch.impl.BytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch
import app.revanced.patches.youtube.misc.videoid.annotation.VideoIdCompatibility
import app.revanced.patches.youtube.misc.videoid.fingerprint.VideoIdFingerprint
import org.jf.dexlib2.iface.instruction.formats.Instruction11x
import org.jf.dexlib2.iface.instruction.OneRegisterInstruction
@Name("video-id-hook")
@Description("hook to detect when the video id changes")
@ -25,12 +27,21 @@ class VideoIdPatch : BytecodePatch(
)
) {
override fun execute(data: BytecodeData): PatchResult {
result = VideoIdFingerprint.result!!
insertMethod = result.mutableMethod
videoIdRegister =
(insertMethod.implementation!!.instructions[result.patternScanResult!!.endIndex + 1] as OneRegisterInstruction).registerA
injectCall("Lapp/revanced/integrations/videoplayer/VideoInformation;->setCurrentVideoId(Ljava/lang/String;)V")
return PatchResultSuccess()
}
companion object {
private lateinit var result: MethodFingerprintResult
private var videoIdRegister: Int = 0
private lateinit var insertMethod: MutableMethod
private var offset = 2
/**
@ -40,12 +51,7 @@ class VideoIdPatch : BytecodePatch(
fun injectCall(
methodDescriptor: String
) {
val result = VideoIdFingerprint.result!!
val method = result.mutableMethod
val videoIdRegister =
(method.implementation!!.instructions[result.patternScanResult!!.endIndex + 1] as Instruction11x).registerA
method.addInstructions(
insertMethod.addInstructions(
result.patternScanResult!!.endIndex + offset, // after the move-result-object
"invoke-static {v$videoIdRegister}, $methodDescriptor"
)

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h5v2h2L12,1h-2v2zM10,18L5,18l5,-6v6zM19,3h-5v2h5v13l-5,-6v9h5c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX=".8"
android:scaleY=".8">
<path
android:fillColor="#FFFFFF"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
</group>
<path
android:fillColor="#FFFFFF"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM19,11c0,4.52 -2.98,8.69 -7,9.93 -4.02,-1.24 -7,-5.41 -7,-9.93L5,6.3l7,-3.11 7,3.11L19,14.17z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M5,5c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L6,4c-0.55,0 -1,0.45 -1,1zM7.41,14L9,14v5c0,0.55 0.45,1 1,1h4c0.55,0 1,-0.45 1,-1v-5h1.59c0.89,0 1.34,-1.08 0.71,-1.71L12.71,7.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l-4.59,4.59c-0.63,0.63 -0.19,1.71 0.7,1.71z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,6c0,-0.55 -0.45,-1 -1,-1L5.82,5l0.66,-3.18 0.02,-0.23c0,-0.31 -0.13,-0.59 -0.33,-0.8L5.38,0 0.44,4.94C0.17,5.21 0,5.59 0,6v6.5c0,0.83 0.67,1.5 1.5,1.5h6.75c0.62,0 1.15,-0.38 1.38,-0.91l2.26,-5.29c0.07,-0.17 0.11,-0.36 0.11,-0.55L12,6zM22.5,10h-6.75c-0.62,0 -1.15,0.38 -1.38,0.91l-2.26,5.29c-0.07,0.17 -0.11,0.36 -0.11,0.55L12,18c0,0.55 0.45,1 1,1h5.18l-0.66,3.18 -0.02,0.24c0,0.31 0.13,0.59 0.33,0.8l0.79,0.78 4.94,-4.94c0.27,-0.27 0.44,-0.65 0.44,-1.06v-6.5c0,-0.83 -0.67,-1.5 -1.5,-1.5z" />
</vector>

View File

@ -0,0 +1,4 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" >
<com.google.android.libraries.youtube.common.ui.TouchImageView android:id="@+id/sponsorblock_button" android:padding="@dimen/controls_overlay_action_button_padding" android:layout_width="@dimen/controls_overlay_action_button_size" android:layout_height="@dimen/controls_overlay_action_button_size" android:layout_marginTop="2dp" android:src="@drawable/ic_sb_logo" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:layout_marginEnd="4dp" android:layout_toStartOf="@+id/player_additional_view_container" style="@style/YouTubePlayerButton"/>
<com.google.android.libraries.youtube.common.ui.TouchImageView android:id="@+id/voting_button" android:padding="@dimen/controls_overlay_action_button_padding" android:layout_width="@dimen/controls_overlay_action_button_size" android:layout_height="@dimen/controls_overlay_action_button_size" android:layout_marginTop="2dp" android:src="@drawable/ic_sb_voting" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:layout_marginEnd="4dp" android:layout_toStartOf="@+id/sponsorblock_button" style="@style/YouTubePlayerButton"/>
</RelativeLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<app.revanced.integrations.sponsorblock.player.ui.NewSegmentLayout android:id="@+id/new_segment_view" android:focusable="true" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/brand_interaction_default_bottom_margin" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true"/>
<app.revanced.integrations.sponsorblock.player.ui.SkipSponsorButton android:id="@+id/skip_sponsor_button" android:focusable="true" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/inline_controls_bottom_bar_height" android:layout_alignParentRight="true" android:layout_alignParentBottom="true"/>
</merge>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:yt="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:gravity="start|center"
android:orientation="vertical"
android:id="@+id/new_segment_container"
android:background="#66000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="36.0dip">
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_rewind"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/player_fast_rewind"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="10.0dip"
android:paddingEnd="5.0dip" />
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_forward"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/player_fast_forward"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="5.0dip"
android:paddingEnd="5.0dip" />
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_adjust"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/ic_sb_adjust"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="5.0dip"
android:paddingEnd="10.0dip" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="36.0dip">
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_compare"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/ic_sb_compare"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="10.0dip"
android:paddingEnd="5.0dip" />
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_edit"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/ic_sb_edit"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="5.0dip"
android:paddingEnd="5.0dip" />
<ImageButton
android:layout_gravity="start|center"
android:id="@+id/new_segment_publish"
android:background="@android:color/transparent"
android:paddingTop="3.0dip"
android:paddingBottom="3.0dip"
android:layout_width="45.0dip"
android:layout_height="36.0dip"
android:src="@drawable/ic_sb_publish"
android:contentDescription="@null"
android:alpha="1.0"
android:paddingStart="5.0dip"
android:paddingEnd="10.0dip" />
</LinearLayout>
</LinearLayout>
</merge>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:yt="http://schemas.android.com/apk/res-auto">
<LinearLayout android:layout_gravity="center_vertical" android:orientation="horizontal" android:id="@+id/skip_sponsor_button_container" android:padding="8dp" android:layout_width="wrap_content" android:layout_height="32dp" android:contentDescription="@string/skip_sponsor">
<com.google.android.libraries.youtube.common.ui.YouTubeTextView android:textSize="@dimen/extra_small_font_size" android:textColor="@color/skip_ad_button_foreground_color" android:layout_gravity="center_vertical" android:id="@+id/skip_sponsor_button_text" android:paddingRight="@dimen/ad_overlay_ad_text_padding" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/skip_sponsor" android:singleLine="true" android:includeFontPadding="false" yt:robotoFont="light"/>
<ImageView android:layout_gravity="center_vertical" android:id="@+id/skip_sponsor_button_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/quantum_ic_skip_next_white_24" android:contentDescription="@null" android:alpha="0.8"/>
</LinearLayout>
</merge>

View File

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sb">Enable SponsorBlock</string>
<string name="enable_sb_sum">SponsorBlock is a crowd-sourced system for skipping annoying parts in YouTube videos</string>
<string name="enable_segmadding">Enable new segment adding</string>
<string name="enable_segmadding_sum">Switch this on to enable experimental segment adding (has button visibility issues)</string>
<string name="diff_segments">What to do with different segments</string>
<string name="general">General</string>
<string name="general_skiptoast">Show a toast when skipping segment automatically</string>
<string name="general_skiptoast_sum">Click to see an example toast</string>
<string name="general_skipcount">Skip count tracking</string>
<string name="general_skipcount_sum">This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment</string>
<string name="general_adjusting">Adjusting new segment step</string>
<string name="general_adjusting_sum">This is the number of milliseconds you can move when you use the time adjustment buttons while adding new segment</string>
<string name="general_min_duration">Minimum segment duration</string>
<string name="general_min_duration_sum">Segments shorter than the set value (in seconds) will not be skipped or shown in the player</string>
<string name="general_uuid">Your unique user id</string>
<string name="general_uuid_sum">This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you</string>
<string name="settings_ie">Import/Export settings</string>
<string name="settings_ie_sum">This is your entire configuration that is applicable in the desktop extension in JSON. This includes your userID, so be sure to share this wisely.</string>
<string name="general_api_url">Change API URL</string>
<string name="general_api_url_sum">The address SponsorBlock uses to make calls to the server. &lt;b>Don\'t change this unless you know what you\'re doing.&lt;/b></string>
<string name="settings_import_successful">Settings were successfully imported</string>
<string name="settings_import_failed">Failed to import settings</string>
<string name="settings_export_failed">Failed to export settings</string>
<string name="general_cache">Cache segments locally</string>
<string name="general_cache_sum">Frequently watched videos (eg. music videos) may store segments in app cache to make skipping segments faster</string>
<string name="general_cache_clear">Clear SponsorBlock segments cache</string>
<string name="segments_sponsor">Sponsor</string>
<string name="segments_sponsor_sum">Paid promotion, paid referrals and direct advertisements</string>
<string name="segments_intermission">Intermission/Intro Animation</string>
<string name="segments_intermission_sum">An interval without actual content. Could be a pause, static frame, repeating animation</string>
<string name="segments_endcards">Endcards/Credits</string>
<string name="segments_endcards_sum">Credits or when the YouTube endcards appear. Not for spoken conclusions</string>
<string name="segments_subscribe">Interaction Reminder (Subscribe)</string>
<string name="segments_subscribe_sum">When there is a short reminder to like, subscribe or follow them in the middle of content</string>
<string name="segments_selfpromo">Unpaid/Self Promotion</string>
<string name="segments_selfpromo_sum">When there is unpaid or self promotion. This includes specific sections about merchandise, donations, or information about who they collaborated with</string>
<string name="segments_nomusic">Music: Non-Music Section</string>
<string name="segments_nomusic_sum">Only for use in music videos. This includes introductions or outros in music videos</string>
<string name="segments_filler">Filler Tangent/Jokes</string>
<string name="segments_filler_sum">Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include context or background details</string>
<string name="skipped_segment">Skipped a sponsor segment</string>
<string name="skipped_sponsor">Skipped sponsor</string>
<string name="skipped_intermission">Skipped intro</string>
<string name="skipped_endcard">Skipped outro</string>
<string name="skipped_subscribe">Skipped annoying reminder</string>
<string name="skipped_selfpromo">Skipped self promotion</string>
<string name="skipped_nomusic">Skipped a non-music section</string>
<string name="skipped_preview">Skipped preview</string>
<string name="skipped_filler">Skipped filler</string>
<string name="skipped_unsubmitted">Skipped unsubmitted segment</string>
<string name="skip_automatically">Skip automatically</string>
<string name="skip_showbutton">Show a skip button</string>
<string name="skip_ignore">Don\'t do anything</string>
<string name="skip_sponsor">Skip segment</string>
<string name="about">About</string>
<string name="about_api">This app uses the API from SponsorBlock</string>
<string name="about_api_sum">Tap to learn more, and see downloads for other platforms at: sponsor.ajay.app</string>
<string name="about_madeby">Integration made by JakubWeg</string>
<string name="tap_skip">Tap to skip</string>
<string name="submit_failed_unknown_error" formatted="false">Unable to submit segments: Status: %d %s</string>
<string name="submit_failed_rate_limit">Can\'t submit the segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="submit_failed_forbidden" formatted="false">Can\'t submit the segment.\n\n%s</string>
<string name="submit_failed_duplicate">Can\'t submit the segment.\nAlready exists</string>
<string name="submit_succeeded">Segment submitted successfully</string>
<string name="submit_started">Submitting segment…</string>
<string name="vote_failed_unknown_error" formatted="false">Unable to vote for segment: Status: %d %s</string>
<string name="vote_failed_rate_limit">Can\'t vote for segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="vote_failed_forbidden" formatted="false">Can\'t vote for segment.\n\n%s</string>
<string name="vote_succeeded">Voted successfully</string>
<string name="vote_started">Voting for segment…</string>
<string name="vote_upvote">Upvote</string>
<string name="vote_downvote">Downvote</string>
<string name="vote_category">Change category</string>
<string name="vote_no_segments">There are no segments to vote for</string>
<string name="enable_voting">Enable voting</string>
<string name="enable_voting_sum">Switch this on to enable voting.</string>
<string name="new_segment_choose_category">Choose the segment category</string>
<string name="new_segment_disabled_category">You\'ve disabled this category in the settings, enable it to be able to submit</string>
<string name="new_segment_title">New SponsorBlock segment</string>
<string name="new_segment_mark_time_as_question" formatted="false">Set %02d:%02d:%04d as the start or end of a new segment?</string>
<string name="new_segment_mark_start">start</string>
<string name="new_segment_mark_end">end</string>
<string name="new_segment_now">now</string>
<string name="new_segment_time_start">Time the segment begins at</string>
<string name="new_segment_time_end">Time the segment ends at</string>
<string name="new_segment_time_start_set">Beginning of segment set</string>
<string name="new_segment_time_end_set">End of segment set</string>
<string name="new_segment_confirm_title">Are the times correct?</string>
<string name="new_segment_confirm_content" formatted="false">The segment lasts from %02d:%02d to %02d:%02d (%d minutes %02d seconds)\nIs it ready to submit?</string>
<string name="new_segment_mark_locations_first">Mark two locations on the time bar first</string>
<string name="new_segment_edit_by_hand_title">Edit timing of segment manually</string>
<string name="new_segment_edit_by_hand_content">Do you want to edit the timing for the start or end of the segment?</string>
<string name="new_segment_edit_by_hand_saved">Done</string>
<string name="new_segment_edit_by_hand_parse_error">Invalid time given</string>
<string name="sb_guidelines_preference_title">View guidelines</string>
<string name="sb_guidelines_preference_sum">Guidelines contain tips and rules about submitting segments</string>
<string name="sb_guidelines_popup_title">There are guidelines</string>
<string name="sb_guidelines_popup_content">It\'s recommended to read the SponsorBlock guidelines before submitting any segment</string>
<string name="sb_guidelines_popup_already_read">Already read</string>
<string name="sb_guidelines_popup_open">Show me</string>
<string name="sb_settings">SponsorBlock settings</string>
<string name="sb_summary">Uses the sponsor.ajay.app API</string>
<string name="microg_notification_settings">Notification settings</string>
<string name="microg_notification_settings_summary">"1. Google device registration and Cloud Messaging need to be enabled for notifications.
2. ReVanced needs to be shown as registered under Cloud Messaging.
3. Current State in Cloud Messaging must be Connected."</string>
<string name="microg_settings">MicroG settings</string>
<string name="revanced_settings">ReVanced settings</string>
<string name="revanced_seekbar_tapping">Seekbar Tapping</string>
<string name="revanced_seekbar_tapping_off">Seekbar Tapping (video progress bar) is disabled</string>
<string name="revanced_seekbar_tapping_on">Seekbar Tapping (video progress bar) is enabled</string>
<string name="pref_subtitles_scale_normal">Normal</string>
<string name="litho_shorts_shelf">Shorts Shelf</string>
<string name="litho_shorts_shelf_off">Shorts Shelf removal is turned off</string>
<string name="litho_shorts_shelf_on">Shorts Shelf removal is turned on</string>
<string name="revanced_create_button_summary_off">Create Button has default visibility</string>
<string name="revanced_create_button_summary_on">Create Button is forcefully disabled</string>
<string name="revanced_create_button_title">Create Button</string>
<string name="litho_community_guidelines">Community Guidelines</string>
<string name="litho_community_guidelines_off">Community Guidelines removal is turned off</string>
<string name="litho_community_guidelines_on">Community Guidelines removal is turned on</string>
<string name="revanced_copy_video_url_summary_off">Copy Link Button is hidden from the player overlay</string>
<string name="revanced_copy_video_url_summary_on">Copy Link Button is shown in the player overlay</string>
<string name="revanced_copy_video_url_timestamp_summary_off">Copy Link Button With Timestamp is hidden from the player overlay</string>
<string name="revanced_copy_video_url_timestamp_summary_on">Copy Link Button With Timestamp is shown in the player overlay</string>
<string name="revanced_copy_video_url_timestamp_title">Copy Link Button With Timestamp</string>
<string name="revanced_copy_video_url_title">Copy Link Button</string>
<string name="revanced_old_style_quality_settings_title">Quality Settings style</string>
<string name="revanced_old_style_quality_settings_summary_off">Using default style video quality settings</string>
<string name="revanced_old_style_quality_settings_summary_on">Using old style video quality settings</string>
<string name="general_time_without_sb">Show time without segments</string>
<string name="general_time_without_sb_sum">This time appears in brackets next to the current time. This shows the total video duration minus any segments.</string>
<string name="general_whitelisting">Channel whitelisting</string>
<string name="general_whitelisting_sum">Use the Segments button under the player to whitelist a channel</string>
<string name="general_browser_button">Enable SB Browser button</string>
<string name="general_browser_button_sum">Clicking this button under the player will open sb.ltn.fi where you can see all the segments on the video.</string>
<string name="segments_preview">Preview/Recap</string>
<string name="segments_preview_sum">Recap of previous episodes, or a preview of what\'s coming up later in the current video or future videos in the same series. Intended for edited together clips that do not provide additional information.</string>
<string name="stats">Stats</string>
<string name="stats_loading">Loading..</string>
<string name="stats_sb_disabled">SponsorBlock is disabled</string>
<string name="stats_username" formatted="false">Your username: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_username_change">Click to change your username</string>
<string name="stats_username_change_unknown_error" formatted="false">Unable to change username: Status: %d %s</string>
<string name="stats_username_changed">Username successfully changed</string>
<string name="stats_submissions">Submissions: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_saved">You\'ve saved people from &lt;b&gt;%s&lt;/b&gt; segments.</string>
<string name="stats_saved_sum">That\'s &lt;b&gt;%s&lt;/b&gt; of their lives. Click to see the leaderboard</string>
<string name="stats_self_saved">You\'ve skipped &lt;b&gt;%s&lt;/b&gt; segments.</string>
<string name="stats_self_saved_sum">That\'s &lt;b&gt;%s&lt;/b&gt;.</string>
<string name="minutes">minutes</string>
<string name="color_change">Are you looking for changing colors?</string>
<string name="color_change_sum">You can now change a category\'s color by clicking on it above.</string>
<string name="color_choose_category">Choose the category</string>
<string name="color_changed">Color changed</string>
<string name="color_reset">Color reset</string>
<string name="color_invalid">Invalid hex code</string>
<string name="change">Change</string>
<string name="reset">Reset</string>
<string name="action_segments">Segments</string>
<string name="action_browser">SB Browser</string>
<string name="api_url_changed">API URL changed</string>
<string name="api_url_reset">API URL reset</string>
<string name="api_url_invalid">Provided API URL is invalid</string>
</resources>