mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-18 00:33:57 +00:00
Compare commits
11 Commits
v5.38.0
...
fix/youtub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
debe93d87a | ||
|
|
3910d9c9b9 | ||
|
|
e0edfdbd97 | ||
|
|
95580f84ec | ||
|
|
2520129ace | ||
|
|
7eeffd3392 | ||
|
|
6c3391164e | ||
|
|
0b8b46c73e | ||
|
|
cbe576bc38 | ||
|
|
3a29f2a805 | ||
|
|
50069c7e05 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## [5.38.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.38.1-dev.1...v5.38.1-dev.2) (2025-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube Music - Spoof video streams:** Remove iPadOS client ([7eeffd3](https://github.com/ReVanced/revanced-patches/commit/7eeffd3392c57555342173103d3a417c038d0970))
|
||||
|
||||
## [5.38.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.38.0...v5.38.1-dev.1) (2025-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Do not use Android Creator for livestreams ([cbe576b](https://github.com/ReVanced/revanced-patches/commit/cbe576bc384ef5f5ee2fa341147925ed0dff568b))
|
||||
|
||||
# [5.38.0](https://github.com/ReVanced/revanced-patches/compare/v5.37.0...v5.38.0) (2025-09-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.extension.music.patches.spoof;
|
||||
|
||||
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
|
||||
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||
@@ -22,6 +23,8 @@ public class SpoofVideoStreamsPatch {
|
||||
VISIONOS
|
||||
);
|
||||
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients, ANDROID_VR_1_43_32);
|
||||
ClientType client = SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setPreferredClient(client);
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package app.revanced.extension.music.settings;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.EnumSetting;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
public class Settings extends BaseSettings {
|
||||
|
||||
@@ -18,4 +22,8 @@ public class Settings extends BaseSettings {
|
||||
|
||||
// Player
|
||||
public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true);
|
||||
|
||||
// Miscellaneous
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
|
||||
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
}
|
||||
|
||||
5
extensions/proguard-rules.pro
vendored
5
extensions/proguard-rules.pro
vendored
@@ -7,3 +7,8 @@
|
||||
-keep class com.google.** {
|
||||
*;
|
||||
}
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-dontwarn java.beans.**
|
||||
-dontwarn jdk.dynalink.**
|
||||
-dontwarn javax.script.**
|
||||
@@ -1,6 +1,4 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -39,7 +37,7 @@ public final class ByteTrieSearch extends TrieSearch<byte[]> {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
public ByteTrieSearch(@NonNull byte[]... patterns) {
|
||||
public ByteTrieSearch(byte[]... patterns) {
|
||||
super(new ByteTrieNode(), patterns);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
/**
|
||||
* Text pattern searching using a prefix tree (trie).
|
||||
@@ -28,7 +26,7 @@ public final class StringTrieSearch extends TrieSearch<String> {
|
||||
}
|
||||
}
|
||||
|
||||
public StringTrieSearch(@NonNull String... patterns) {
|
||||
public StringTrieSearch(String... patterns) {
|
||||
super(new StringTrieNode(), patterns);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.extension.youtube;
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -8,8 +7,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Searches for a group of different patterns using a trie (prefix tree).
|
||||
/**Searches for a group of different patterns using a trie (prefix tree).
|
||||
* Can significantly speed up searching for multiple patterns.
|
||||
*/
|
||||
public abstract class TrieSearch<T> {
|
||||
@@ -136,7 +134,7 @@ public abstract class TrieSearch<T> {
|
||||
* @param patternLength Length of the pattern.
|
||||
* @param callback Callback, where a value of NULL indicates to always accept a pattern match.
|
||||
*/
|
||||
private void addPattern(@NonNull T pattern, int patternIndex, int patternLength,
|
||||
private void addPattern(T pattern, int patternIndex, int patternLength,
|
||||
@Nullable TriePatternMatchedCallback<T> callback) {
|
||||
if (patternIndex == patternLength) { // Reached the end of the pattern.
|
||||
if (endOfPatternCallback == null) {
|
||||
@@ -308,13 +306,13 @@ public abstract class TrieSearch<T> {
|
||||
private final List<T> patterns = new ArrayList<>();
|
||||
|
||||
@SafeVarargs
|
||||
TrieSearch(@NonNull TrieNode<T> root, @NonNull T... patterns) {
|
||||
TrieSearch(TrieNode<T> root, T... patterns) {
|
||||
this.root = Objects.requireNonNull(root);
|
||||
addPatterns(patterns);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void addPatterns(@NonNull T... patterns) {
|
||||
public final void addPatterns(T... patterns) {
|
||||
for (T pattern : patterns) {
|
||||
addPattern(pattern);
|
||||
}
|
||||
@@ -325,7 +323,7 @@ public abstract class TrieSearch<T> {
|
||||
*
|
||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||
*/
|
||||
public void addPattern(@NonNull T pattern) {
|
||||
public void addPattern(T pattern) {
|
||||
addPattern(pattern, root.getTextLength(pattern), null);
|
||||
}
|
||||
|
||||
@@ -333,31 +331,31 @@ public abstract class TrieSearch<T> {
|
||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||
* @param callback Callback to determine if searching should halt when a match is found.
|
||||
*/
|
||||
public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback<T> callback) {
|
||||
public void addPattern(T pattern, TriePatternMatchedCallback<T> callback) {
|
||||
addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
||||
void addPattern(T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
||||
if (patternLength == 0) return; // Nothing to match
|
||||
|
||||
patterns.add(pattern);
|
||||
root.addPattern(pattern, 0, patternLength, callback);
|
||||
}
|
||||
|
||||
public final boolean matches(@NonNull T textToSearch) {
|
||||
public final boolean matches(T textToSearch) {
|
||||
return matches(textToSearch, 0);
|
||||
}
|
||||
|
||||
public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) {
|
||||
public boolean matches(T textToSearch, Object callbackParameter) {
|
||||
return matches(textToSearch, 0, root.getTextLength(textToSearch),
|
||||
Objects.requireNonNull(callbackParameter));
|
||||
}
|
||||
|
||||
public boolean matches(@NonNull T textToSearch, int startIndex) {
|
||||
public boolean matches(T textToSearch, int startIndex) {
|
||||
return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
|
||||
}
|
||||
|
||||
public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) {
|
||||
public final boolean matches(T textToSearch, int startIndex, int endIndex) {
|
||||
return matches(textToSearch, startIndex, endIndex, null);
|
||||
}
|
||||
|
||||
@@ -370,11 +368,11 @@ public abstract class TrieSearch<T> {
|
||||
* @param callbackParameter Optional parameter passed to the callbacks.
|
||||
* @return If any pattern matched, and it's callback halted searching.
|
||||
*/
|
||||
public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
|
||||
public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
|
||||
return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
|
||||
}
|
||||
|
||||
private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex,
|
||||
private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex,
|
||||
@Nullable Object callbackParameter) {
|
||||
if (endIndex > textToSearchLength) {
|
||||
throw new IllegalArgumentException("endIndex: " + endIndex
|
||||
@@ -5,9 +5,6 @@ import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
|
||||
/**
|
||||
* Settings shared across multiple apps.
|
||||
* <p>
|
||||
@@ -31,13 +28,4 @@ public class BaseSettings {
|
||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||
// Client type must be last spoof setting due to cyclic references.
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
|
||||
static {
|
||||
if (SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS_UNPLUGGED) {
|
||||
Logger.printInfo(() -> "Migrating from iOS Unplugged to iPadOS");
|
||||
SPOOF_VIDEO_STREAMS_CLIENT_TYPE.save(ClientType.IPADOS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ public enum ClientType {
|
||||
"132.0.6808.3",
|
||||
"1.61.48",
|
||||
false,
|
||||
false,
|
||||
"Android VR 1.61"
|
||||
),
|
||||
/**
|
||||
@@ -50,7 +49,6 @@ public enum ClientType {
|
||||
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
|
||||
"107.0.5284.2",
|
||||
"1.43.32",
|
||||
ANDROID_VR_1_61_48.requiresAuth,
|
||||
ANDROID_VR_1_61_48.useAuth,
|
||||
"Android VR 1.43"
|
||||
),
|
||||
@@ -71,7 +69,6 @@ public enum ClientType {
|
||||
"132.0.6779.0",
|
||||
"23.47.101",
|
||||
true,
|
||||
true,
|
||||
"Android Creator"
|
||||
),
|
||||
/**
|
||||
@@ -86,7 +83,6 @@ public enum ClientType {
|
||||
"0.1",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
||||
false,
|
||||
false,
|
||||
"visionOS"
|
||||
),
|
||||
/**
|
||||
@@ -111,25 +107,7 @@ public enum ClientType {
|
||||
"19.22.3",
|
||||
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
|
||||
false,
|
||||
false,
|
||||
"iPadOS"
|
||||
),
|
||||
/**
|
||||
* Obsolete and broken client. Here only to migrate data.
|
||||
*/
|
||||
@Deprecated
|
||||
IOS_UNPLUGGED(
|
||||
33,
|
||||
"IOS_UNPLUGGED",
|
||||
"Apple",
|
||||
"iPhone16,2",
|
||||
"iOS",
|
||||
"18.2.22C152",
|
||||
"8.49",
|
||||
"dummy user-agent",
|
||||
true,
|
||||
true,
|
||||
"iOS TV"
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -197,12 +175,6 @@ public enum ClientType {
|
||||
*/
|
||||
public final String clientVersion;
|
||||
|
||||
/**
|
||||
* If this client requires authentication and does not work
|
||||
* if logged out or in incognito mode.
|
||||
*/
|
||||
public final boolean requiresAuth;
|
||||
|
||||
/**
|
||||
* If the client should use authentication if available.
|
||||
*/
|
||||
@@ -227,7 +199,6 @@ public enum ClientType {
|
||||
@NonNull String buildId,
|
||||
@NonNull String cronetVersion,
|
||||
String clientVersion,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
String friendlyName) {
|
||||
this.id = id;
|
||||
@@ -241,7 +212,6 @@ public enum ClientType {
|
||||
this.buildId = buildId;
|
||||
this.cronetVersion = cronetVersion;
|
||||
this.clientVersion = clientVersion;
|
||||
this.requiresAuth = requiresAuth;
|
||||
this.useAuth = useAuth;
|
||||
this.friendlyName = friendlyName;
|
||||
|
||||
@@ -267,7 +237,6 @@ public enum ClientType {
|
||||
String osVersion,
|
||||
String clientVersion,
|
||||
String userAgent,
|
||||
boolean requiresAuth,
|
||||
boolean useAuth,
|
||||
String friendlyName) {
|
||||
this.id = id;
|
||||
@@ -278,7 +247,6 @@ public enum ClientType {
|
||||
this.osVersion = osVersion;
|
||||
this.clientVersion = clientVersion;
|
||||
this.userAgent = userAgent;
|
||||
this.requiresAuth = requiresAuth;
|
||||
this.useAuth = useAuth;
|
||||
this.friendlyName = friendlyName;
|
||||
this.packageName = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
@@ -17,14 +18,6 @@ import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofVideoStreamsPatch {
|
||||
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
|
||||
private static final boolean FIX_HLS_CURRENT_TIME = SPOOF_STREAMING_DATA
|
||||
&& BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.VISIONOS;
|
||||
|
||||
@Nullable
|
||||
private static volatile AppLanguage languageOverride;
|
||||
|
||||
/**
|
||||
* Domain used for internet connectivity verification.
|
||||
* It has an empty response body and is only used to check for a 204 response code.
|
||||
@@ -40,6 +33,13 @@ public class SpoofVideoStreamsPatch {
|
||||
private static final String INTERNET_CONNECTION_CHECK_URI_STRING = "https://www.google.com/gen_204";
|
||||
private static final Uri INTERNET_CONNECTION_CHECK_URI = Uri.parse(INTERNET_CONNECTION_CHECK_URI_STRING);
|
||||
|
||||
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
|
||||
@Nullable
|
||||
private static volatile AppLanguage languageOverride;
|
||||
|
||||
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_61_48;
|
||||
|
||||
/**
|
||||
* @return If this patch was included during patching.
|
||||
*/
|
||||
@@ -47,8 +47,9 @@ public class SpoofVideoStreamsPatch {
|
||||
return false; // Modified during patching.
|
||||
}
|
||||
|
||||
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
||||
return isPatchIncluded() && BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
@Nullable
|
||||
public static AppLanguage getLanguageOverride() {
|
||||
return languageOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,9 +60,14 @@ public class SpoofVideoStreamsPatch {
|
||||
languageOverride = language;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AppLanguage getLanguageOverride() {
|
||||
return languageOverride;
|
||||
public static void setPreferredClient(ClientType client) {
|
||||
preferredClient = Objects.requireNonNull(client);
|
||||
}
|
||||
|
||||
public static boolean spoofingToClientWithNoMultiAudioStreams() {
|
||||
return isPatchIncluded()
|
||||
&& SPOOF_STREAMING_DATA
|
||||
&& preferredClient != ClientType.IPADOS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,8 +282,7 @@ public class SpoofVideoStreamsPatch {
|
||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
// Since all current clients are un-authenticated, this works for all spoof clients.
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get();
|
||||
return BaseSettings.SPOOF_VIDEO_STREAMS.get() && !preferredClient.useAuth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ final class PlayerRoutes {
|
||||
JSONObject context = new JSONObject();
|
||||
|
||||
AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride();
|
||||
if (language == null || BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32) {
|
||||
if (language == null || clientType == ANDROID_VR_1_43_32) {
|
||||
// Force original audio has not overrode the language.
|
||||
// Or if YT has fallen over to the very last client (VR 1.43), then always
|
||||
// use the app language because forcing an audio stream of specific languages
|
||||
// Or if YT has fallen over to the last unauthenticated client (VR 1.43), then
|
||||
// always use the app language because forcing an audio stream of specific languages
|
||||
// can sometimes fail so it's better to try and load something rather than nothing.
|
||||
language = BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.extension.shared.spoof.requests;
|
||||
|
||||
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
|
||||
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -13,12 +14,18 @@ import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
@@ -93,6 +100,16 @@ public class StreamingDataRequest {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Strings found in the response if the video is a livestream.
|
||||
*/
|
||||
private static final ByteTrieSearch liveStreamBufferSearch = new ByteTrieSearch(
|
||||
convertStringsToBytes(
|
||||
"yt_live_broadcast",
|
||||
"yt_premiere_broadcast"
|
||||
)
|
||||
);
|
||||
|
||||
private static volatile ClientType lastSpoofedClientType;
|
||||
|
||||
public static String getLastSpoofedClientName() {
|
||||
@@ -160,7 +177,7 @@ public class StreamingDataRequest {
|
||||
}
|
||||
}
|
||||
|
||||
if (!authHeadersIncludes && clientType.requiresAuth) {
|
||||
if (!authHeadersIncludes && clientType.useAuth) {
|
||||
Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType
|
||||
+ " videoId: " + videoId);
|
||||
return null;
|
||||
@@ -221,9 +238,13 @@ public class StreamingDataRequest {
|
||||
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
||||
baos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
lastSpoofedClientType = clientType;
|
||||
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
|
||||
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
|
||||
} else {
|
||||
lastSpoofedClientType = clientType;
|
||||
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
|
||||
@@ -2,6 +2,19 @@ dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:youtube:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:0.24.8")
|
||||
implementation("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
implementation("com.squareup.okio:okio:3.7.0") // Newer okio use Kotlin 2.0 which Patches does not yet use.
|
||||
implementation("com.github.TeamNewPipe:nanojson:e9d656ddb49a412a5a0a5d5ef20ca7ef09549996")
|
||||
implementation("io.reactivex.rxjava3:rxandroid:3.0.2")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.youtube.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
|
||||
abstract class FilterGroup<T> {
|
||||
final static class FilterGroupResult {
|
||||
|
||||
@@ -5,9 +5,9 @@ import androidx.annotation.NonNull;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import app.revanced.extension.youtube.ByteTrieSearch;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.TrieSearch;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
|
||||
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.ByteTrieSearch;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.TrieSearch;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -12,7 +12,7 @@ import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.TrieSearch;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
|
||||
/**
|
||||
* Searches for video id's in the proto buffer of Shorts dislike.
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
package app.revanced.extension.youtube.patches.spoof
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import app.revanced.extension.shared.Utils
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleEmitter
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo
|
||||
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
|
||||
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
||||
import java.io.Closeable
|
||||
import java.time.Instant
|
||||
|
||||
import com.grack.nanojson.JsonObject
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
// TODO: Remove
|
||||
fun test(){
|
||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl);
|
||||
with(StreamInfo.getInfo("https://youtube.com/watch?v=dQw4w9WgXcQ")) {
|
||||
this.videoStreams.first().content
|
||||
this.videoOnlyStreams.first().content
|
||||
this.audioStreams.first().content
|
||||
this.subtitles.first().isAutoGenerated
|
||||
}
|
||||
}
|
||||
|
||||
object BuildConfig {
|
||||
const val DEBUG = true
|
||||
}
|
||||
|
||||
fun parseChallengeData(rawChallengeData: String): String {
|
||||
val scrambled = JsonParser.array().from(rawChallengeData)
|
||||
|
||||
val challengeData = if (scrambled.size > 1 && scrambled.isString(1)) {
|
||||
val descrambled = descramble(scrambled.getString(1))
|
||||
JsonParser.array().from(descrambled)
|
||||
} else {
|
||||
scrambled.getArray(0)
|
||||
}
|
||||
|
||||
val messageId = challengeData.getString(0)
|
||||
val interpreterHash = challengeData.getString(3)
|
||||
val program = challengeData.getString(4)
|
||||
val globalName = challengeData.getString(5)
|
||||
val clientExperimentsStateBlob = challengeData.getString(7)
|
||||
|
||||
val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData.getArray(1, null)?.find { it is String }
|
||||
val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData.getArray(2, null)?.find { it is String }
|
||||
|
||||
return JsonWriter.string(
|
||||
JsonObject.builder()
|
||||
.value("messageId", messageId)
|
||||
.`object`("interpreterJavascript")
|
||||
.value("privateDoNotAccessOrElseSafeScriptWrappedValue", privateDoNotAccessOrElseSafeScriptWrappedValue)
|
||||
.value("privateDoNotAccessOrElseTrustedResourceUrlWrappedValue", privateDoNotAccessOrElseTrustedResourceUrlWrappedValue)
|
||||
.end()
|
||||
.value("interpreterHash", interpreterHash)
|
||||
.value("program", program)
|
||||
.value("globalName", globalName)
|
||||
.value("clientExperimentsStateBlob", clientExperimentsStateBlob)
|
||||
.done()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript
|
||||
* `Uint8Array` that can be embedded directly in JavaScript code, and an [Int] representing the
|
||||
* duration of this token in seconds.
|
||||
*/
|
||||
fun parseIntegrityTokenData(rawIntegrityTokenData: String): Pair<String, Long> {
|
||||
val integrityTokenData = JsonParser.array().from(rawIntegrityTokenData)
|
||||
return base64ToU8(integrityTokenData.getString(0)) to integrityTokenData.getLong(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript
|
||||
* `Uint8Array` that can be embedded directly in JavaScript code.
|
||||
*/
|
||||
fun stringToU8(identifier: String): String {
|
||||
return newUint8Array(identifier.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a poToken encoded as a sequence of bytes represented as integers separated by commas
|
||||
* (e.g. "97,98,99" would be "abc"), which is the output of `Uint8Array::toString()` in JavaScript,
|
||||
* and converts it to the specific base64 representation for poTokens.
|
||||
*/
|
||||
fun u8ToBase64(poToken: String): String {
|
||||
return poToken.split(",")
|
||||
.map { it.toUByte().toByte() }
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
.base64()
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the scrambled challenge, decodes it from base64, adds 97 to each byte.
|
||||
*/
|
||||
private fun descramble(scrambledChallenge: String): String {
|
||||
return base64ToByteString(scrambledChallenge)
|
||||
.map { (it + 97).toByte() }
|
||||
.toByteArray()
|
||||
.decodeToString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string encoded in the specific base64 representation used by YouTube, and
|
||||
* returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code.
|
||||
*/
|
||||
private fun base64ToU8(base64: String): String {
|
||||
return newUint8Array(base64ToByteString(base64))
|
||||
}
|
||||
|
||||
private fun newUint8Array(contents: ByteArray): String {
|
||||
return "new Uint8Array([" + contents.joinToString(separator = ",") { it.toUByte().toString() } + "])"
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a base64 string encoded in the specific base64 representation used by YouTube.
|
||||
*/
|
||||
private fun base64ToByteString(base64: String): ByteArray {
|
||||
val base64Mod = base64
|
||||
.replace('-', '+')
|
||||
.replace('_', '/')
|
||||
.replace('.', '=')
|
||||
|
||||
return (base64Mod.decodeBase64() ?: throw PoTokenException("Cannot base64 decode"))
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
class PoTokenException(message: String) : Exception(message)
|
||||
|
||||
// to be thrown if the WebView provided by the system is broken
|
||||
class BadWebViewException(message: String) : Exception(message)
|
||||
|
||||
fun buildExceptionForJsError(error: String): Exception {
|
||||
return if (error.contains("SyntaxError"))
|
||||
BadWebViewException(error)
|
||||
else
|
||||
PoTokenException(error)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This interface was created to allow for multiple methods to generate poTokens in the future (e.g.
|
||||
* via WebView and via a local DOM implementation)
|
||||
*/
|
||||
interface PoTokenGenerator : Closeable {
|
||||
/**
|
||||
* Generates a poToken for the provided identifier, using the `integrityToken` and
|
||||
* `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be
|
||||
* called multiple times.
|
||||
*/
|
||||
fun generatePoToken(identifier: String): Single<String>
|
||||
|
||||
/**
|
||||
* @return whether the `integrityToken` is expired, in which case all tokens generated by
|
||||
* [generatePoToken] will be invalid
|
||||
*/
|
||||
fun isExpired(): Boolean
|
||||
|
||||
interface Factory {
|
||||
/**
|
||||
* Initializes a [PoTokenGenerator] by loading the BotGuard VM, running it, and obtaining
|
||||
* an `integrityToken`. Can then be used multiple times to generate multiple poTokens with
|
||||
* [generatePoToken].
|
||||
*
|
||||
* @param context used e.g. to load the HTML asset or to instantiate a WebView
|
||||
*/
|
||||
fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object PoTokenProviderImpl : PoTokenProvider {
|
||||
val TAG = PoTokenProviderImpl::class.simpleName
|
||||
private var webViewBadImpl = false // whether the system has a bad WebView implementation
|
||||
|
||||
private object WebPoTokenGenLock
|
||||
private var webPoTokenVisitorData: String? = null
|
||||
private var webPoTokenStreamingPot: String? = null
|
||||
private var webPoTokenGenerator: PoTokenGenerator? = null
|
||||
|
||||
override fun getWebClientPoToken(videoId: String): PoTokenResult? {
|
||||
if (!webViewBadImpl) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return getWebClientPoToken(videoId = videoId, forceRecreate = false)
|
||||
} catch (e: RuntimeException) {
|
||||
// RxJava's Single wraps exceptions into RuntimeErrors, so we need to unwrap them here
|
||||
when (val cause = e.cause) {
|
||||
is BadWebViewException -> {
|
||||
Log.e(TAG, "Could not obtain poToken because WebView is broken", e)
|
||||
webViewBadImpl = true
|
||||
return null
|
||||
}
|
||||
null -> throw e
|
||||
else -> throw cause // includes PoTokenException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forceRecreate whether to force the recreation of [webPoTokenGenerator], to be used in
|
||||
* case the current [webPoTokenGenerator] threw an error last time
|
||||
* [PoTokenGenerator.generatePoToken] was called
|
||||
*/
|
||||
private fun getWebClientPoToken(videoId: String, forceRecreate: Boolean): PoTokenResult {
|
||||
// just a helper class since Kotlin does not have builtin support for 4-tuples
|
||||
data class Quadruple<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)
|
||||
|
||||
val (poTokenGenerator, visitorData, streamingPot, hasBeenRecreated) =
|
||||
synchronized(WebPoTokenGenLock) {
|
||||
val shouldRecreate = webPoTokenGenerator == null || forceRecreate ||
|
||||
webPoTokenGenerator!!.isExpired()
|
||||
|
||||
if (shouldRecreate) {
|
||||
|
||||
val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient()
|
||||
innertubeClientRequestInfo.clientInfo.clientVersion =
|
||||
YoutubeParsingHelper.getClientVersion()
|
||||
|
||||
webPoTokenVisitorData = YoutubeParsingHelper.getVisitorDataFromInnertube(
|
||||
innertubeClientRequestInfo,
|
||||
NewPipe.getPreferredLocalization(),
|
||||
NewPipe.getPreferredContentCountry(),
|
||||
YoutubeParsingHelper.getYouTubeHeaders(),
|
||||
YoutubeParsingHelper.YOUTUBEI_V1_URL,
|
||||
null,
|
||||
false
|
||||
)
|
||||
// close the current webPoTokenGenerator on the main thread
|
||||
webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } }
|
||||
|
||||
// create a new webPoTokenGenerator
|
||||
webPoTokenGenerator = PoTokenWebView
|
||||
.newPoTokenGenerator(Utils.getContext()).blockingGet()
|
||||
|
||||
// The streaming poToken needs to be generated exactly once before generating
|
||||
// any other (player) tokens.
|
||||
webPoTokenStreamingPot = webPoTokenGenerator!!
|
||||
.generatePoToken(webPoTokenVisitorData!!).blockingGet()
|
||||
}
|
||||
|
||||
return@synchronized Quadruple(
|
||||
webPoTokenGenerator!!,
|
||||
webPoTokenVisitorData!!,
|
||||
webPoTokenStreamingPot!!,
|
||||
shouldRecreate
|
||||
)
|
||||
}
|
||||
|
||||
val playerPot = try {
|
||||
// Not using synchronized here, since poTokenGenerator would be able to generate
|
||||
// multiple poTokens in parallel if needed. The only important thing is for exactly one
|
||||
// visitorData/streaming poToken to be generated before anything else.
|
||||
poTokenGenerator.generatePoToken(videoId).blockingGet()
|
||||
} catch (throwable: Throwable) {
|
||||
if (hasBeenRecreated) {
|
||||
// the poTokenGenerator has just been recreated (and possibly this is already the
|
||||
// second time we try), so there is likely nothing we can do
|
||||
throw throwable
|
||||
} else {
|
||||
// retry, this time recreating the [webPoTokenGenerator] from scratch;
|
||||
// this might happen for example if NewPipe goes in the background and the WebView
|
||||
// content is lost
|
||||
Log.e(TAG, "Failed to obtain poToken, retrying", throwable)
|
||||
return getWebClientPoToken(videoId = videoId, forceRecreate = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"poToken for $videoId: playerPot=$playerPot, " +
|
||||
"streamingPot=$streamingPot, visitor_data=$visitorData"
|
||||
)
|
||||
}
|
||||
|
||||
return PoTokenResult(visitorData, playerPot, streamingPot)
|
||||
}
|
||||
|
||||
override fun getWebEmbedClientPoToken(videoId: String): PoTokenResult? = null
|
||||
|
||||
override fun getAndroidClientPoToken(videoId: String): PoTokenResult? = null
|
||||
|
||||
override fun getIosClientPoToken(videoId: String): PoTokenResult? = null
|
||||
}
|
||||
|
||||
|
||||
class PoTokenWebView private constructor(
|
||||
context: Context,
|
||||
// to be used exactly once only during initialization!
|
||||
private val generatorEmitter: SingleEmitter<PoTokenGenerator>,
|
||||
) : PoTokenGenerator {
|
||||
private val webView = WebView(context)
|
||||
private val disposables = CompositeDisposable() // used only during initialization
|
||||
private val poTokenEmitters = mutableListOf<Pair<String, SingleEmitter<String>>>()
|
||||
private lateinit var expirationInstant: Instant
|
||||
|
||||
//region Initialization
|
||||
init {
|
||||
val webViewSettings = webView.settings
|
||||
//noinspection SetJavaScriptEnabled we want to use JavaScript!
|
||||
webViewSettings.javaScriptEnabled = true
|
||||
webViewSettings.safeBrowsingEnabled = false
|
||||
webViewSettings.userAgentString = USER_AGENT
|
||||
webViewSettings.blockNetworkLoads = true // the WebView does not need internet access
|
||||
|
||||
// so that we can run async functions and get back the result
|
||||
webView.addJavascriptInterface(this, JS_INTERFACE)
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onConsoleMessage(m: ConsoleMessage): Boolean {
|
||||
if (m.message().contains("Uncaught")) {
|
||||
// There should not be any uncaught errors while executing the code, because
|
||||
// everything that can fail is guarded by try-catch. Therefore, this likely
|
||||
// indicates that there was a syntax error in the code, i.e. the WebView only
|
||||
// supports a really old version of JS.
|
||||
|
||||
val fmt = "\"${m.message()}\", source: ${m.sourceId()} (${m.lineNumber()})"
|
||||
val exception = BadWebViewException(fmt)
|
||||
Log.e(TAG, "This WebView implementation is broken: $fmt")
|
||||
|
||||
onInitializationErrorCloseAndCancel(exception)
|
||||
popAllPoTokenEmitters().forEach { (_, emitter) -> emitter.onError(exception) }
|
||||
}
|
||||
return super.onConsoleMessage(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called right after instantiating [PoTokenWebView] to perform the actual
|
||||
* initialization. This will asynchronously go through all the steps needed to load BotGuard,
|
||||
* run it, and obtain an `integrityToken`.
|
||||
*/
|
||||
private fun loadHtmlAndObtainBotguard(context: Context) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "loadHtmlAndObtainBotguard() called")
|
||||
}
|
||||
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
val html = context.assets.open("po_token.html").bufferedReader()
|
||||
.use { it.readText() }
|
||||
return@fromCallable html
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ html ->
|
||||
webView.loadDataWithBaseURL(
|
||||
"https://www.youtube.com",
|
||||
html.replaceFirst(
|
||||
"</script>",
|
||||
// calls downloadAndRunBotguard() when the page has finished loading
|
||||
"\n$JS_INTERFACE.downloadAndRunBotguard()</script>"
|
||||
),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null,
|
||||
)
|
||||
},
|
||||
this::onInitializationErrorCloseAndCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippet appended to the HTML page content in
|
||||
* [loadHtmlAndObtainBotguard] after the WebView content has been loaded.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun downloadAndRunBotguard() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "downloadAndRunBotguard() called")
|
||||
}
|
||||
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/Create",
|
||||
"[ \"$REQUEST_KEY\" ]",
|
||||
) { responseBody ->
|
||||
val parsedChallengeData = parseChallengeData(responseBody)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
data = $parsedChallengeData
|
||||
runBotGuard(data).then(function (result) {
|
||||
this.webPoSignalOutput = result.webPoSignalOutput
|
||||
$JS_INTERFACE.onRunBotguardResult(result.botguardResponse)
|
||||
}, function (error) {
|
||||
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
|
||||
})
|
||||
} catch (error) {
|
||||
$JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack)
|
||||
}""",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippets from either
|
||||
* [downloadAndRunBotguard] or [onRunBotguardResult].
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onJsInitializationError(error: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "Initialization error from JavaScript: $error")
|
||||
}
|
||||
onInitializationErrorCloseAndCancel(buildExceptionForJsError(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during initialization by the JavaScript snippet from [downloadAndRunBotguard] after
|
||||
* obtaining the BotGuard execution output [botguardResponse].
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onRunBotguardResult(botguardResponse: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "botguardResponse: $botguardResponse")
|
||||
}
|
||||
makeBotguardServiceRequest(
|
||||
"https://www.youtube.com/api/jnn/v1/GenerateIT",
|
||||
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]",
|
||||
) { responseBody ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "GenerateIT response: $responseBody")
|
||||
}
|
||||
val (integrityToken, expirationTimeInSeconds) = parseIntegrityTokenData(responseBody)
|
||||
|
||||
// leave 10 minutes of margin just to be sure
|
||||
expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds - 600)
|
||||
|
||||
webView.evaluateJavascript(
|
||||
"this.integrityToken = $integrityToken"
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "initialization finished, expiration=${expirationTimeInSeconds}s")
|
||||
}
|
||||
generatorEmitter.onSuccess(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Obtaining poTokens
|
||||
override fun generatePoToken(identifier: String): Single<String> =
|
||||
Single.create { emitter ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "generatePoToken() called with identifier $identifier")
|
||||
}
|
||||
runOnMainThread(emitter) {
|
||||
addPoTokenEmitter(identifier, emitter)
|
||||
val u8Identifier = stringToU8(identifier)
|
||||
webView.evaluateJavascript(
|
||||
"""try {
|
||||
identifier = "$identifier"
|
||||
u8Identifier = $u8Identifier
|
||||
poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier)
|
||||
poTokenU8String = ""
|
||||
for (i = 0; i < poTokenU8.length; i++) {
|
||||
if (i != 0) poTokenU8String += ","
|
||||
poTokenU8String += poTokenU8[i]
|
||||
}
|
||||
$JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)
|
||||
} catch (error) {
|
||||
$JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack)
|
||||
}""",
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the
|
||||
* JavaScript `obtainPoToken()` function.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onObtainPoTokenError(identifier: String, error: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e(TAG, "obtainPoToken error from JavaScript: $error")
|
||||
}
|
||||
popPoTokenEmitter(identifier)?.onError(buildExceptionForJsError(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the JavaScript snippet from [generatePoToken] with the original identifier and the
|
||||
* result of the JavaScript `obtainPoToken()` function.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun onObtainPoTokenResult(identifier: String, poTokenU8: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Generated poToken (before decoding): identifier=$identifier poTokenU8=$poTokenU8")
|
||||
}
|
||||
val poToken = try {
|
||||
u8ToBase64(poTokenU8)
|
||||
} catch (t: Throwable) {
|
||||
popPoTokenEmitter(identifier)?.onError(t)
|
||||
return
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Generated poToken: identifier=$identifier poToken=$poToken")
|
||||
}
|
||||
popPoTokenEmitter(identifier)?.onSuccess(poToken)
|
||||
}
|
||||
|
||||
override fun isExpired(): Boolean {
|
||||
return Instant.now().isAfter(expirationInstant)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Handling multiple emitters
|
||||
/**
|
||||
* Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that
|
||||
* multiple poToken requests can be generated invparallel, and the results will be notified to
|
||||
* the right emitters.
|
||||
*/
|
||||
private fun addPoTokenEmitter(identifier: String, emitter: SingleEmitter<String>) {
|
||||
synchronized(poTokenEmitters) {
|
||||
poTokenEmitters.add(Pair(identifier, emitter))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and removes from the [poTokenEmitters] list a [SingleEmitter] based on its
|
||||
* [identifier]. The emitter is supposed to be used immediately after to either signal a success
|
||||
* or an error.
|
||||
*/
|
||||
private fun popPoTokenEmitter(identifier: String): SingleEmitter<String>? {
|
||||
return synchronized(poTokenEmitters) {
|
||||
poTokenEmitters.indexOfFirst { it.first == identifier }.takeIf { it >= 0 }?.let {
|
||||
poTokenEmitters.removeAt(it).second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears [poTokenEmitters] and returns its previous contents. The emitters are supposed to be
|
||||
* used immediately after to either signal a success or an error.
|
||||
*/
|
||||
private fun popAllPoTokenEmitters(): List<Pair<String, SingleEmitter<String>>> {
|
||||
return synchronized(poTokenEmitters) {
|
||||
val result = poTokenEmitters.toList()
|
||||
poTokenEmitters.clear()
|
||||
result
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Utils
|
||||
/**
|
||||
* Makes a POST request to [url] with the given [data] by setting the correct headers. Calls
|
||||
* [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response
|
||||
* does not have HTTP code 200, therefore this is supposed to be used only during
|
||||
* initialization. Calls [handleResponseBody] with the response body if the response is
|
||||
* successful. The request is performed in the background and a disposable is added to
|
||||
* [disposables].
|
||||
*/
|
||||
private fun makeBotguardServiceRequest(
|
||||
url: String,
|
||||
data: String,
|
||||
handleResponseBody: (String) -> Unit,
|
||||
) {
|
||||
disposables.add(
|
||||
Single.fromCallable {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.doOutput = true
|
||||
|
||||
// headers
|
||||
connection.setRequestProperty("User-Agent", USER_AGENT)
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
connection.setRequestProperty("Content-Type", "application/json+protobuf")
|
||||
connection.setRequestProperty("x-goog-api-key", GOOGLE_API_KEY)
|
||||
connection.setRequestProperty("x-user-agent", "grpc-web-javascript/0.1")
|
||||
|
||||
// body
|
||||
connection.outputStream.use { os ->
|
||||
os.writer().write(data);
|
||||
}
|
||||
|
||||
// response
|
||||
|
||||
return@fromCallable connection
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ connection ->
|
||||
val httpCode = connection.responseCode
|
||||
if (httpCode != 200) {
|
||||
onInitializationErrorCloseAndCancel(
|
||||
PoTokenException("Invalid response code: $httpCode")
|
||||
)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
val responseBody = connection.inputStream.bufferedReader().use { it.readText() }
|
||||
connection.disconnect()
|
||||
|
||||
handleResponseBody(responseBody)
|
||||
},
|
||||
this::onInitializationErrorCloseAndCancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any error happening during initialization, releasing resources and sending the error
|
||||
* to [generatorEmitter].
|
||||
*/
|
||||
private fun onInitializationErrorCloseAndCancel(error: Throwable) {
|
||||
runOnMainThread(generatorEmitter) {
|
||||
close()
|
||||
generatorEmitter.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases all [webView] and [disposables] resources.
|
||||
*/
|
||||
override fun close() {
|
||||
disposables.dispose()
|
||||
|
||||
webView.clearHistory()
|
||||
// clears RAM cache and disk cache (globally for all WebViews)
|
||||
webView.clearCache(true)
|
||||
|
||||
// ensures that the WebView isn't doing anything when destroying it
|
||||
webView.loadUrl("about:blank")
|
||||
|
||||
webView.onPause()
|
||||
webView.removeAllViews()
|
||||
webView.destroy()
|
||||
}
|
||||
//endregion
|
||||
|
||||
companion object : PoTokenGenerator.Factory {
|
||||
private val TAG = PoTokenWebView::class.simpleName
|
||||
// Public API key used by BotGuard, which has been got by looking at BotGuard requests
|
||||
private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR
|
||||
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
|
||||
private const val JS_INTERFACE = "PoTokenWebView"
|
||||
|
||||
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> =
|
||||
Single.create { emitter ->
|
||||
runOnMainThread(emitter) {
|
||||
val potWv = PoTokenWebView(context, emitter)
|
||||
potWv.loadHtmlAndObtainBotguard(context)
|
||||
emitter.setDisposable(potWv.disposables)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and
|
||||
* if the `post` fails emits an error on [emitterIfPostFails].
|
||||
*/
|
||||
private fun runOnMainThread(
|
||||
emitterIfPostFails: SingleEmitter<out Any>,
|
||||
runnable: Runnable,
|
||||
) {
|
||||
if (!Handler(Looper.getMainLooper()).post(runnable)) {
|
||||
emitterIfPostFails.onError(PoTokenException("Could not run on main thread"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.shared.spoof.requests.StreamingDataRequest;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofVideoStreamsPatch {
|
||||
@@ -22,14 +22,13 @@ public class SpoofVideoStreamsPatch {
|
||||
List<ClientType> availableClients = List.of(
|
||||
ANDROID_VR_1_61_48,
|
||||
VISIONOS,
|
||||
IPADOS,
|
||||
// Creator must be next to last, because livestreams fetch successfully but don't playback.
|
||||
ANDROID_CREATOR,
|
||||
// VR 1.43 must be last as spoof streaming data handles it slightly differently.
|
||||
ANDROID_VR_1_43_32
|
||||
ANDROID_VR_1_43_32,
|
||||
IPADOS
|
||||
);
|
||||
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients,
|
||||
BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get());
|
||||
ClientType client = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setPreferredClient(client);
|
||||
StreamingDataRequest.setClientOrderToUse(availableClients, client);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import app.revanced.extension.shared.settings.LongSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
|
||||
@@ -356,6 +357,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_61_48, true, parent(SPOOF_VIDEO_STREAMS));
|
||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
||||
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.spoof.ClientType;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||
@@ -69,7 +70,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
ClientType clientType = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
ClientType clientType = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get();
|
||||
if (currentClientType == clientType) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
||||
org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
version = 5.38.0
|
||||
version = 5.38.1-dev.2
|
||||
|
||||
@@ -38,7 +38,7 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
|
||||
)
|
||||
},
|
||||
executeBlock = {
|
||||
addResources("shared", "misc.spoof.spoofVideoStreamsPatch")
|
||||
addResources("music", "misc.fix.playback.spoofVideoStreamsPatch")
|
||||
|
||||
PreferenceScreen.MISC.addPreferences(
|
||||
PreferenceScreenPreference(
|
||||
|
||||
@@ -89,10 +89,16 @@ Davam et düyməsinə toxun və optimallaşdırma dəyişikliklərin qəbul et."
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string name="revanced_spoof_video_streams_screen_title">Video yayımları saxtalaşdır</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Oynatma problemlərin önləmək üçün qəbuledici video yayımların saxtalaşdır</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Video yayımların saxtalaşdır</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Oynatma problemlərin önləmək üçün qəbuledici video yayımların saxtalaşdır</string>
|
||||
<string name="revanced_spoof_video_streams_title">Video yayımları saxtalaşdır</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">"Video yayımları saxtalaşdırılıb
|
||||
|
||||
Əgər YouTube Premium istifadəçisisinizsə, bu tənzimlənmə tələb olunmaya bilər"</string>
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Video yayımları saxtalaşmayıb
|
||||
|
||||
Oynatma işləməyə bilər"</string>
|
||||
<string name="revanced_spoof_video_streams_user_dialog_message">Bu tənzimləməni qapatmaq oynatma problemlərinə səbəb ola bilər.</string>
|
||||
<string name="revanced_spoof_video_streams_client_type_title">İlkin qəbuledici</string>
|
||||
</patch>
|
||||
<patch id="misc.debugging.enableDebuggingPatch">
|
||||
@@ -713,6 +719,9 @@ Bu seçimi dəyişdirmə işə düşmürsə, Gizli rejimə keçməyə çalışı
|
||||
<string name="revanced_hide_player_flyout_audio_track_summary_on">Səs axını menyusu gizlidir</string>
|
||||
<string name="revanced_hide_player_flyout_audio_track_summary_off">Səs axını menyusu göstərilir</string>
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Səs trek menyusu gizlidir
|
||||
|
||||
Səs treki menyusunu göstərmək üçün \"Video yayımları saxtalaşdır\"ı iPadOS-a dəyiş"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">\"VR-da İzləni\" gizlət</string>
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">VR menyusunda izləmə gizlidir</string>
|
||||
@@ -1541,6 +1550,7 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Android saxtalaşdırma yan təsirləri</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Səs treki menyusu əlçatmazdır
|
||||
• Sabit səs səviyyəsi yoxdur"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video 01:00-da dayana bilər və ya bəzi bölgələrdə mövcud olmaya bilər</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Təcrübi qəbuledici və hər vaxt işləməyi dayandıra bilər</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• AV1 video kodlayıcı yoxdur</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Giriş edilməyəndə və ya gizli rejimdə uşaq videoları oynadıla bilməz</string>
|
||||
@@ -1549,6 +1559,7 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Qəbuledici nerd üçün Statistikada gizlidir</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Səs yayım dili</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Xüsusi səs dilini seçmək üçün \"Orijinal səs dilini zorlanı\" qapat</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
@@ -1556,18 +1567,31 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
|
||||
<string name="revanced_settings_music_screen_0_about_title">Haqqında</string>
|
||||
<string name="revanced_settings_music_screen_1_ads_title">Reklamlar</string>
|
||||
<string name="revanced_settings_music_screen_2_general_title">Ümumi</string>
|
||||
<string name="revanced_settings_music_screen_3_player_title">Oyunçu</string>
|
||||
<string name="revanced_settings_music_screen_3_player_title">Oynadıcı</string>
|
||||
<string name="revanced_settings_music_screen_4_misc_title">Çoxvariantlı</string>
|
||||
</patch>
|
||||
<patch id="ad.video.hideVideoAdsPatch">
|
||||
<string name="revanced_music_hide_video_ads_title">Video reklamlarını gizlət</string>
|
||||
<string name="revanced_music_hide_video_ads_summary_on">Video reklamları gizlidir</string>
|
||||
<string name="revanced_music_hide_video_ads_summary_off">Video reklamları görünür</string>
|
||||
</patch>
|
||||
<patch id="interaction.permanentrepeat.permanentRepeatPatch">
|
||||
<string name="revanced_music_play_permanent_repeat_title">Kəsintisiz təkrarlamanı aktivləşdir</string>
|
||||
<string name="revanced_music_play_permanent_repeat_summary_on">Kəsintisiz təkrarlama aktivdir</string>
|
||||
<string name="revanced_music_play_permanent_repeat_summary_off">Kəsintisiz təkrarlama qapalıdır</string>
|
||||
</patch>
|
||||
<patch id="layout.compactheader.hideCategoryBar">
|
||||
<string name="revanced_music_hide_category_bar_title">Kateqoriya cizgisin gizlət</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_on">Kateqoriya cizgisi gizlidir</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_off">Kateqoriya cizgisi görünür</string>
|
||||
</patch>
|
||||
<patch id="layout.premium.hideGetPremiumPatch">
|
||||
<string name="revanced_music_hide_get_premium_label_title"> \'Musiqi Premiumu Əldə et\' etiketini gizlət</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_on">Etiket gizlidir</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_off">Etiket görünür</string>
|
||||
</patch>
|
||||
<patch id="layout.upgradebutton.hideUpgradeButtonPatch">
|
||||
<string name="revanced_music_hide_upgrade_button_title">Təkmilləşdirmə düyməsini gizlət</string>
|
||||
<string name="revanced_music_hide_upgrade_button_summary_on">Düymə gizlidir</string>
|
||||
<string name="revanced_music_hide_upgrade_button_summary_off">Düymə görünür</string>
|
||||
</patch>
|
||||
|
||||
@@ -721,7 +721,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_hide_player_flyout_audio_track_summary_on">Κρυμμένο</string>
|
||||
<string name="revanced_hide_player_flyout_audio_track_summary_off">Εμφανίζεται</string>
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Το μενού «Κομμάτι ήχου» είναι κρυμμένο
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Κρυμμένο
|
||||
|
||||
Για να εμφανίζεται το μενού κομματιού ήχου, αλλάξτε την «Παραποίηση ροών βίντεο» σε iPadOS"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
@@ -1550,7 +1550,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Παρενέργειες παραποίησης σε Android</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Το μενού «Κομμάτι ήχου» λείπει
|
||||
• Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Το βίντεο ενδέχεται να σταματήσει στο 1:00 ή ενδέχεται να μην είναι διαθέσιμο σε ορισμένες περιοχές</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Το βίντεο ενδέχεται να σταματήσει στο 1:00 ή να μην είναι διαθέσιμο σε ορισμένες περιοχές</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Πειραματικός πελάτης και μπορεί να σταματήσει να λειτουργεί ανά πάσα στιγμή</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• Δεν υπάρχει ο κωδικοποιητής βίντεο AV1</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Τα βίντεο για παιδιά ενδέχεται να μην αναπαράγονται αν είστε αποσυνδεδεμένοι ή σε λειτουργία ανώνυμης περιήγησης</string>
|
||||
@@ -1559,12 +1559,12 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Το πρόγραμμα πελάτη δεν εμφανίζεται στο μενού «Στατιστικά για σπασίκλες»</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Γλώσσα ροής ήχου</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Για να επιλέξετε μια συγκεκριμένη γλώσσα ήχου, απενεργοποιήστε το \'Επιβολή αρχικής γλώσσας ήχου\'</string>
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Για να επιλέξετε μια συγκεκριμένη γλώσσα ήχου, απενεργοποιήστε το «Εξαναγκασμός αρχικής γλώσσας ήχου»</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
<patch id="misc.settings.settingsPatch">
|
||||
<string name="revanced_settings_music_screen_0_about_title">Ομάδα προγραμματιστών</string>
|
||||
<string name="revanced_settings_music_screen_0_about_title">Σχετικά με</string>
|
||||
<string name="revanced_settings_music_screen_1_ads_title">Διαφημίσεις</string>
|
||||
<string name="revanced_settings_music_screen_2_general_title">Γενικά</string>
|
||||
<string name="revanced_settings_music_screen_3_player_title">Οθόνη αναπαραγωγής</string>
|
||||
@@ -1581,17 +1581,17 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_music_play_permanent_repeat_summary_off">Η μόνιμη επανάληψη είναι απενεργοποιημένη</string>
|
||||
</patch>
|
||||
<patch id="layout.compactheader.hideCategoryBar">
|
||||
<string name="revanced_music_hide_category_bar_title">Απόκρυψη γραμμής κατηγοριών</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_on">Η γραμμή κατηγοριών είναι κρυμμένη</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_off">Η γραμμή κατηγοριών εμφανίζεται</string>
|
||||
<string name="revanced_music_hide_category_bar_title">Γραμμή κατηγοριών</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_on">Κρυμμένη</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_off">Εμφανίζεται</string>
|
||||
</patch>
|
||||
<patch id="layout.premium.hideGetPremiumPatch">
|
||||
<string name="revanced_music_hide_get_premium_label_title">Απόκρυψη ετικέτας «Απόκτηση Music Premium»</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_on">Η ετικέτα είναι κρυμμένη</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_off">Η ετικέτα εμφανίζεται</string>
|
||||
<string name="revanced_music_hide_get_premium_label_title">Ετικέτα «Αποκτήστε το Music Premium»</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_on">Κρυμμένη</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_off">Εμφανίζεται</string>
|
||||
</patch>
|
||||
<patch id="layout.upgradebutton.hideUpgradeButtonPatch">
|
||||
<string name="revanced_music_hide_upgrade_button_title">Απόκρυψη κουμπιού αναβάθμισης</string>
|
||||
<string name="revanced_music_hide_upgrade_button_title">Κουμπί «Αναβάθμιση»</string>
|
||||
<string name="revanced_music_hide_upgrade_button_summary_on">Κρυμμένο</string>
|
||||
<string name="revanced_music_hide_upgrade_button_summary_off">Εμφανίζεται</string>
|
||||
</patch>
|
||||
|
||||
@@ -94,10 +94,10 @@ Appuyez sur le bouton Continuer et autorisez les modifications."</string>
|
||||
<string name="revanced_spoof_video_streams_title">Falsifier les flux vidéo</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">"Les flux vidéo sont falsifiés
|
||||
|
||||
Si vous avez YouTube Premium, il est possible que ce paramètre ne soit pas nécessaire"</string>
|
||||
Si vous êtes abonné à YouTube Premium, ce paramètre n'est peut-être pas nécessaire"</string>
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Les flux vidéo ne sont pas falsifiés
|
||||
|
||||
La lecture est susceptible de ne pas fonctionner"</string>
|
||||
Il est possible que la lecture ne fonctionne pas"</string>
|
||||
<string name="revanced_spoof_video_streams_user_dialog_message">La désactivation de ce paramètre peut entraîner des problèmes de lecture.</string>
|
||||
<string name="revanced_spoof_video_streams_client_type_title">Client par défaut</string>
|
||||
</patch>
|
||||
@@ -1063,9 +1063,9 @@ Cette fonctionnalité fonctionne de manière optimale avec une qualité vidéo d
|
||||
<string name="revanced_sb_general_min_duration">Durée minimale d\'un segment</string>
|
||||
<string name="revanced_sb_general_min_duration_sum">Les segments dont la durée est inférieure à cette valeur (en secondes) ne seront ni affichés ni ignorés</string>
|
||||
<string name="revanced_sb_general_min_duration_invalid">Durée invalide</string>
|
||||
<string name="revanced_sb_general_uuid">Votre ID privé d\'utilisateur</string>
|
||||
<string name="revanced_sb_general_uuid">Votre ID d\'utilisateur privé</string>
|
||||
<string name="revanced_sb_general_uuid_sum">Cet identifiant doit rester confidentiel. Cet identifiant est comme un mot de passe et ne doit être partagé avec qui que ce soit. Si quelqu\'un venait à mettre la main dessus, il pourrait usurper votre identité.</string>
|
||||
<string name="revanced_sb_general_uuid_invalid">L\'ID privé d\'utilisateur doit comporter au moins 30 caractères</string>
|
||||
<string name="revanced_sb_general_uuid_invalid">ID d\'utilisateur privé trop court (30+ caractères)</string>
|
||||
<string name="revanced_sb_general_api_url">Modifier l\'URL de l\'API</string>
|
||||
<string name="revanced_sb_general_api_url_sum">L\'adresse utilisée par SponsorBlock pour contacter son serveur</string>
|
||||
<string name="revanced_sb_api_url_reset">URL de l\'API réinitialisée</string>
|
||||
@@ -1074,11 +1074,11 @@ Cette fonctionnalité fonctionne de manière optimale avec une qualité vidéo d
|
||||
<string name="revanced_sb_settings_ie">Importer/Exporter les paramètres</string>
|
||||
<string name="revanced_sb_settings_copy">Copier</string>
|
||||
<string name="revanced_sb_settings_ie_sum">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et les autres plateformes SponsorBlock</string>
|
||||
<string name="revanced_sb_settings_ie_sum_warning">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et d\'autres plateformes SponsorBlock. Votre ID privé d\'utilisateur y est présent, partagez-la avec prudence.</string>
|
||||
<string name="revanced_sb_settings_ie_sum_warning">Votre configuration SponsorBlock au format JSON qui peut être importée/exportée vers ReVanced et d\'autres plateformes SponsorBlock. Votre ID d\'utilisateur privé y est présent, partagez-la avec prudence.</string>
|
||||
<string name="revanced_sb_settings_import_successful">Paramètres importés avec succès</string>
|
||||
<string name="revanced_sb_settings_import_failed">Échec de l\'importation : %s</string>
|
||||
<string name="revanced_sb_settings_export_failed">Échec de l\'exportation : %s</string>
|
||||
<string name="revanced_sb_settings_revanced_export_user_id_warning">"Vos paramètres contiennent un ID privé d'utilisateur.
|
||||
<string name="revanced_sb_settings_revanced_export_user_id_warning">"Vos paramètres contiennent un ID d'utilisateur SponsorBlock privé.
|
||||
|
||||
Votre ID d'utilisateur est comme un mot de passe et ne doit jamais être partagé."</string>
|
||||
<string name="revanced_sb_settings_revanced_export_user_id_warning_dismiss">Ne plus afficher</string>
|
||||
|
||||
@@ -721,7 +721,7 @@ Jika mengubah setelan ini tidak berpengaruh, coba beralih ke mode Penyamaran."</
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Menu trek audio disembunyikan
|
||||
|
||||
Untuk menampilkan menu trek audio, ubah 'Spoof stream video' ke iPadOS"</string>
|
||||
Untuk menampilkan menu trek audio, ubah 'Palsukan aliran video' ke iPadOS"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">Sembunyikan Tonton di VR</string>
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Menu tonton di VR disembunyikan</string>
|
||||
@@ -1548,9 +1548,9 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"</string>
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string name="revanced_spoof_video_streams_about_title">Efek samping pemalsuan</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Efek samping pemalsuan Android</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Menu trek audio tidak ada
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Menu trek audio hilang
|
||||
• Volume stabil tidak tersedia"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video mungkin berhenti pada pukul 1:00, atau mungkin tidak tersedia di beberapa wilayah</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video mungkin berhenti pada menit 1:00, atau mungkin tidak tersedia di beberapa wilayah</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Klien eksperimental dan dapat berhenti berfungsi kapan saja</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• Tidak ada codec video AV1</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Video anak-anak mungkin tidak dapat diputar saat keluar atau dalam mode penyamaran</string>
|
||||
|
||||
@@ -1558,7 +1558,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Android 변경에 따른 부작용</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• 오디오 트랙 메뉴가 표시되지 않습니다
|
||||
• 안정적인 볼륨을 사용할 수 없습니다"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• 동영상이 1:00에서 중지되거나 일부 지역에서 제공되지 않을 수 있습니다</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• 동영상이 1:00에 멈출 수 있으며, 일부 지역에서는 이용이 불가능할 수 있습니다</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• 실험용 클라이언트이며 언제든지 작동이 중단될 수 있습니다</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• AV1 코덱이 지원되지 않습니다</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Kids 동영상은 로그인을 하지 않았거나 시크릿 모드에서는 재생되지 않을 수 있습니다</string>
|
||||
@@ -1567,7 +1567,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">동영상 스트림을 가져오는 데 사용되는 클라이언트가 전문 통계에서 표시되지 않습니다</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">오디오 스트림 언어</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">특정 오디오 언어를 선택하려면 \"원본 오디오 언어 강제 적용\"을 끄십시오.</string>
|
||||
<string name="revanced_spoof_video_streams_language_not_available">특정 오디오 스트림 언어를 선택하려면, \'원본 오디오 스트림 언어 강제로 활성화하기\'를 끄세요</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
|
||||
@@ -89,13 +89,13 @@ Dodirnite dugme „Nastavi” i dozvolite promene optimizacije."</string>
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string name="revanced_spoof_video_streams_screen_title">Lažirani video strimovi</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Lažiranje klijenta video strimova da bi se sprečili problemi sa reprodukcijom</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Lažiranje video tokova</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Lažirajte video tokove klijenta da biste sprečili probleme sa reprodukcijom.</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Lažirani video strimovi</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Lažiranje video strimova klijenta da bi se sprečili problemi sa reprodukcijom</string>
|
||||
<string name="revanced_spoof_video_streams_title">Lažirani video strimovi</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">"Video strimovi su lažirani
|
||||
|
||||
Ako ste korisnik YouTube Premiuma, ovo podešavanje možda neće biti potrebno"</string>
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Video tokovi nisu lažirani
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Video strimovi nisu lažirani
|
||||
|
||||
Reprodukcija možda neće raditi"</string>
|
||||
<string name="revanced_spoof_video_streams_user_dialog_message">Isključivanje ovog podešavanja može izazvati probleme sa reprodukcijom.</string>
|
||||
@@ -721,7 +721,7 @@ Ako se promena ove opcije ne primeni, pokušajte da pređete u režim bez arhivi
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Meni „Audio snimak” je skriven
|
||||
|
||||
Da biste prikazali meni „Audio snimak”, promenite „Lažirani video strimovi” u iPadOS"</string>
|
||||
Da biste prikazali meni „Audio snimak”, promenite opciju „Lažirani video strimovi” na iPadOS"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">Sakrij dugme „Gledaj u VR”</string>
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Dugme „Gledaj u VR” je skriveno</string>
|
||||
@@ -1550,7 +1550,7 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Neželjeni efekti lažiranja na Android</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Meni „Audio snimak” nedostaje
|
||||
• Opcija „Ujednačena jačina zvuka” nije dostupna"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video se može zaustaviti u 1:00, ili možda neće biti dostupan u nekim regionima</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Video se može zaustaviti u 1:00 ili možda neće biti dostupan u nekim regionima</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Eksperimentalni klijent i može prestati da radi bilo kada</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• Nema video kodeka AV1</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Videi za decu se možda neće puštati kada ste odjavljeni ili u režimu bez arhiviranja</string>
|
||||
@@ -1559,12 +1559,12 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Tip klijenta je skriven u „Statistici za znalce”</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Jezik audio strima</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Da biste odabrali određeni audio jezik, isključite \'Prisiljavanje originalnog audio jezika\'</string>
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Da biste izabrali određeni jezik zvuka, isključite opciju „Prisili originalni jezik zvuka”</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
<patch id="misc.settings.settingsPatch">
|
||||
<string name="revanced_settings_music_screen_0_about_title">O</string>
|
||||
<string name="revanced_settings_music_screen_0_about_title">O programu</string>
|
||||
<string name="revanced_settings_music_screen_1_ads_title">Oglasi</string>
|
||||
<string name="revanced_settings_music_screen_2_general_title">Opšte</string>
|
||||
<string name="revanced_settings_music_screen_3_player_title">Plejer</string>
|
||||
@@ -1586,7 +1586,7 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_off">Traka kategorija je prikazana</string>
|
||||
</patch>
|
||||
<patch id="layout.premium.hideGetPremiumPatch">
|
||||
<string name="revanced_music_hide_get_premium_label_title">Sakrij oznaku „Preuzmi Music Premium”</string>
|
||||
<string name="revanced_music_hide_get_premium_label_title">Sakrij oznaku „Nabavite Music Premium”</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_on">Oznaka je skrivena</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_off">Oznaka je prikazana</string>
|
||||
</patch>
|
||||
|
||||
@@ -89,13 +89,13 @@ Second \"item\" text"</string>
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string name="revanced_spoof_video_streams_screen_title">Лажирани видео стримови</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Лажирање клијента видео стримова да би се спречили проблеми са репродукцијом</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Фалсификујте видео стримове</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Фалсификујте клијентске видео стримове да бисте спречили проблеме са репродукцијом</string>
|
||||
<string name="revanced_spoof_video_streams_screen_title">Лажирани видео стримови</string>
|
||||
<string name="revanced_spoof_video_streams_screen_summary">Лажирање видео стримова клијента да би се спречили проблеми са репродукцијом</string>
|
||||
<string name="revanced_spoof_video_streams_title">Лажирани видео стримови</string>
|
||||
<string name="revanced_spoof_video_streams_summary_on">"Видео стримови су лажирани
|
||||
|
||||
Ако сте корисник YouTube Premium-а, ово подешавање можда неће бити потребно"</string>
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Видео стримови нису фалсификовани
|
||||
<string name="revanced_spoof_video_streams_summary_off">"Видео стримови нису лажирани
|
||||
|
||||
Репродукција можда неће радити"</string>
|
||||
<string name="revanced_spoof_video_streams_user_dialog_message">Искључивање овог подешавања може изазвати проблеме са репродукцијом.</string>
|
||||
@@ -721,7 +721,7 @@ Second \"item\" text"</string>
|
||||
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
|
||||
<string name="revanced_hide_player_flyout_audio_track_not_available">"Мени „Аудио снимак” је скривен
|
||||
|
||||
Да бисте приказали мени „Аудио снимак”, промените „Лажирани видео стримови” на iPadOS"</string>
|
||||
Да бисте приказали мени „Аудио снимак”, промените опцију „Лажирани видео стримови” на iPadOS"</string>
|
||||
<!-- 'Watch in VR' should be translated using the same localized wording YouTube displays for the menu item. -->
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_title">Сакриј дугме „Гледај у ВР”</string>
|
||||
<string name="revanced_hide_player_flyout_watch_in_vr_summary_on">Дугме „Гледај у ВР” је скривено</string>
|
||||
@@ -1553,7 +1553,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_title">Нежељени ефекти лажирања на Android</string>
|
||||
<string name="revanced_spoof_video_streams_about_android_summary">"• Мени „Аудио снимак” недостаје
|
||||
• Опција „Уједначена јачина звука” није доступна"</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Видео се може зауставити на 1:00, или можда неће бити доступан у неким регионима</string>
|
||||
<string name="revanced_spoof_video_streams_about_ipados_summary">• Видео се може зауставити на 1:00 или можда неће бити доступан у неким регионима</string>
|
||||
<string name="revanced_spoof_video_streams_about_experimental">• Експериментални клијент и може престати да ради било када</string>
|
||||
<string name="revanced_spoof_video_streams_about_no_av1">• Нема видео кодека AV1</string>
|
||||
<string name="revanced_spoof_video_streams_about_kids_videos">• Видеи за децу се можда неће пуштати када сте одјављени или у режиму без архивирања</string>
|
||||
@@ -1562,7 +1562,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Тип клијента је скривен у „Статистици за зналце”</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Језик аудио стрима</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Да бисте изабрали одређени аудио језик, искључите „Принудно коришћење оригиналног аудио језика“</string>
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Да бисте изабрали одређени језик звука, искључите опцију „Присили оригинални језик звука”</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
@@ -1589,7 +1589,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_music_hide_category_bar_summary_off">Трака категорија је приказана</string>
|
||||
</patch>
|
||||
<patch id="layout.premium.hideGetPremiumPatch">
|
||||
<string name="revanced_music_hide_get_premium_label_title">Сакриј ознаку „Преузми Music Premium”</string>
|
||||
<string name="revanced_music_hide_get_premium_label_title">Сакриј ознаку „Набавите Music Premium”</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_on">Ознака је скривена</string>
|
||||
<string name="revanced_music_hide_get_premium_label_summary_off">Ознака је приказана</string>
|
||||
</patch>
|
||||
|
||||
@@ -1559,7 +1559,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary_off">Тип клієнта приховано у вікні \"Статистика для сисадмінів\"</string>
|
||||
<string name="revanced_spoof_video_streams_language_title">Мова звукової доріжки</string>
|
||||
<!-- 'Force original audio language' should use the same text as revanced_force_original_audio_title -->
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Щоб вибрати певну аудіомову, вимкніть \"Примусово використовувати оригінальну аудіомову\"</string>
|
||||
<string name="revanced_spoof_video_streams_language_not_available">Щоб вибрати певну звукову доріжку, вимкніть \"Примусово оригінальна мова звукової доріжки\"</string>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
|
||||
@@ -121,6 +121,20 @@
|
||||
<item>ZH</item>
|
||||
</string-array>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="music">
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
||||
<item>Android VR</item>
|
||||
<item>visionOS</item>
|
||||
</string-array>
|
||||
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
|
||||
<item>ANDROID_VR_1_43_32</item>
|
||||
<item>VISIONOS</item>
|
||||
</string-array>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="youtube">
|
||||
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
|
||||
<string-array name="revanced_spoof_video_streams_client_type_entries">
|
||||
<item>Android VR</item>
|
||||
@@ -133,8 +147,6 @@
|
||||
<item>IPADOS</item>
|
||||
</string-array>
|
||||
</patch>
|
||||
</app>
|
||||
<app id="youtube">
|
||||
<patch id="interaction.swipecontrols.swipeControlsResourcePatch">
|
||||
<string-array name="revanced_swipe_overlay_style_entries">
|
||||
<item>@string/revanced_swipe_overlay_style_entry_1</item>
|
||||
|
||||
Reference in New Issue
Block a user