diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/YouTubeMusicHostActivity.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/YouTubeMusicHostActivity.java index 6378a065e..1e884b7ba 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/YouTubeMusicHostActivity.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/YouTubeMusicHostActivity.java @@ -27,9 +27,9 @@ public class YouTubeMusicHostActivity extends BaseHostActivity { } @Override - public void finish() { - if (!handleFinish()) { - super.finish(); + public void onBackPressed() { + if (handleBackPress()) { + super.onBackPressed(); } } @@ -106,14 +106,10 @@ public class YouTubeMusicHostActivity extends BaseHostActivity { } /** - * Injection point. - *

- * Overrides {@link Activity#finish()} of the injection Activity. - * - * @return if the original activity finish method should be allowed to run. + * @return Whether pressing the back button should be allowed. */ @Override - protected boolean handleFinish() { - return YouTubeMusicSearchViewController.handleFinish(searchViewController); + protected boolean handleBackPress() { + return YouTubeMusicSearchViewController.handleBackPress(searchViewController); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/search/YouTubeMusicSearchViewController.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/search/YouTubeMusicSearchViewController.java index 3bec2e500..49f1ff3d6 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/search/YouTubeMusicSearchViewController.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/search/YouTubeMusicSearchViewController.java @@ -41,13 +41,12 @@ public class YouTubeMusicSearchViewController extends BaseSearchViewController { // This method can be empty or handle music-specific preferences if any. } - // Static method for handling Activity finish - public static boolean handleFinish(YouTubeMusicSearchViewController searchViewController) { + public static boolean handleBackPress(YouTubeMusicSearchViewController searchViewController) { if (searchViewController != null && searchViewController.isSearchActive()) { searchViewController.closeSearch(); - return true; + return false; } - return false; + return true; } // Adapter to wrap YouTubeMusicPreferenceFragment to BasePreferenceFragment interface. diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeClient.kt index 59f99e091..aa8ebbcfa 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeClient.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeClient.kt @@ -203,6 +203,11 @@ object YouTubeClient { * 2. Over the past few months, BotGuard PoToken has not been used on WEB_SAFARI (iPadOS). * * It may still not work for some users. + * + * Update: + * Playback issue occurs when the value of 'iosPlayerConfig.requestIosguardDataAfterPlaybackStarts' in the player response is 'true'. + * It appears that blocking the '/att/get' endpoint can prevent this, but I'm not sure. + * (More samples are needed) */ private const val PACKAGE_NAME_IPADOS = "com.google.ios.youtube" @@ -252,16 +257,6 @@ object YouTubeClient { */ private const val CLIENT_VERSION_TVHTML5 = "7.20250917.13.00" private const val USER_AGENT_TVHTML5 = - "Mozilla/5.0(SMART-TV; Linux; Tizen 4.0.0.2) AppleWebkit/605.1.15 (KHTML, like Gecko) SamsungBrowser/9.2 TV Safari/605.1.15" - - - // TVHTML5 SIMPLY - /** - * Video not playable: None. - * Note: Only 'Authorization' is supported. - */ - private const val CLIENT_VERSION_TVHTML5_SIMPLY = "1.0" - private const val USER_AGENT_TVHTML5_SIMPLY = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; Xbox)" @@ -512,34 +507,13 @@ object YouTubeClient { TV( id = 7, clientVersion = CLIENT_VERSION_TVHTML5, - clientPlatform = CLIENT_PLATFORM_TV, + clientPlatform = CLIENT_PLATFORM_GAME_CONSOLE, userAgent = USER_AGENT_TVHTML5, requireJS = true, refererFormat = CLIENT_REFERER_FORMAT_TV, clientName = "TVHTML5", friendlyName = "TV" ), - TV_SIMPLY( - id = 75, - clientVersion = CLIENT_VERSION_TVHTML5_SIMPLY, - clientPlatform = CLIENT_PLATFORM_GAME_CONSOLE, - userAgent = USER_AGENT_TVHTML5_SIMPLY, - requireJS = true, - clientName = "TVHTML5_SIMPLY", - refererFormat = CLIENT_REFERER_FORMAT_TV, - friendlyName = "TV Simply" - ), - TV_SIMPLY_NO_AUTH( - id = 75, - clientVersion = CLIENT_VERSION_TVHTML5_SIMPLY, - clientPlatform = CLIENT_PLATFORM_GAME_CONSOLE, - userAgent = USER_AGENT_TVHTML5_SIMPLY, - requireJS = true, - clientName = "TVHTML5_SIMPLY", - refererFormat = CLIENT_REFERER_FORMAT_TV, - supportsCookies = false, - friendlyName = "TV Simply No Auth" - ), // Unused client. TV_EMBEDDED( @@ -580,17 +554,15 @@ object YouTubeClient { ANDROID_CREATOR, IPADOS, TV, - TV_SIMPLY, MWEB, IOS_DEPRECATED, ) val CLIENT_ORDER_TO_USE_JS_PREFER_TV: Array = arrayOf( - TV_SIMPLY, + TV, ANDROID_VR, VISIONOS, ANDROID_CREATOR, IPADOS, - TV, MWEB, IOS_DEPRECATED, ) diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/utils/ThrottlingParameterUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/utils/ThrottlingParameterUtils.java index 7ac69f741..b48c348da 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/utils/ThrottlingParameterUtils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/utils/ThrottlingParameterUtils.java @@ -62,11 +62,11 @@ public class ThrottlingParameterUtils { /** * Hardcoded javascript url path (Mobile Web). */ - private static final String PLAYER_JS_HARDCODED_URL_PATH_MOBILE_WEB = "b66835e2"; + private static final String PLAYER_JS_HARDCODED_URL_PATH_MOBILE_WEB = "0004de42"; /** * Hardcoded javascript url path (TV). */ - private static final String PLAYER_JS_HARDCODED_URL_PATH_TV = "b66835e2"; + private static final String PLAYER_JS_HARDCODED_URL_PATH_TV = "0004de42"; /** * Regular expression pattern to find variables used in JavaScript url. */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index 4d16df70e..11af082ff 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -332,7 +332,7 @@ public class SpoofStreamingDataPatch { *

* It takes about 3-5 seconds to download the JavaScript and initialize the Cipher class. * Initialize it before the video starts. - * Used for {@link ClientType#TV}, {@link ClientType#TV_SIMPLY} and {@link ClientType#TV_EMBEDDED}. + * Used for {@link ClientType#TV} and {@link ClientType#TV_EMBEDDED}. */ public static void initializeJavascript() { if (SPOOF_STREAMING_DATA_USE_JS) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt index f82029d6a..7f9ad715c 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt @@ -358,100 +358,96 @@ class StreamingDataRequest private constructor( private fun setAudioTrackLanguage( videoId: String, requestHeader: Map, + isMobileWeb: Boolean, ) { - for (clientType in arrayOf(ClientType.ANDROID, ClientType.TV_SIMPLY_NO_AUTH, ClientType.ANDROID_VR)) { - val startTime = System.currentTimeMillis() - Logger.printDebug { "Fetching audio track for: $videoId using client: $clientType" } + // On Mobile Web, if an invalid language code is used, it will fall back to English. + // Only use the YouTube API to fetch the default audio track language code when the client is Mobile Web. + if (isMobileWeb) { + for (clientType in arrayOf(ClientType.ANDROID, ClientType.ANDROID_VR)) { + val startTime = System.currentTimeMillis() + Logger.printDebug { "Fetching audio track for: $videoId using client: $clientType" } - try { - val requestBody = if (clientType.requireJS) { - createJSRequestBody( - clientType = clientType, - videoId = videoId, - language = "en", - isGVS = false, - ) - } else { - createApplicationRequestBody( + try { + val requestBody = createApplicationRequestBody( clientType = clientType, videoId = videoId, language = "en" ) - } - val connection = - getInnerTubeResponseConnectionFromRoute( - GET_AUDIO_TRACK, - clientType, - requestHeader - ) + val connection = + getInnerTubeResponseConnectionFromRoute( + GET_AUDIO_TRACK, + clientType, + requestHeader + ) - connection.setFixedLengthStreamingMode(requestBody.size) - connection.outputStream.write(requestBody) + connection.setFixedLengthStreamingMode(requestBody.size) + connection.outputStream.write(requestBody) - val responseCode = connection.responseCode - if (responseCode != 200) { - connection.disconnect() - continue - } - val json = Requester.parseJSONObjectAndDisconnect(connection) + val responseCode = connection.responseCode + if (responseCode != 200) { + connection.disconnect() + continue + } + val json = Requester.parseJSONObjectAndDisconnect(connection) - if (!json.has("captions")) { - continue - } - val captions = - json.getJSONObject("captions") - if (!captions.has("playerCaptionsTracklistRenderer")) { - continue - } - val playerCaptionsTracklistRenderer = - captions.getJSONObject("playerCaptionsTracklistRenderer") - if (!playerCaptionsTracklistRenderer.has("audioTracks")) { - continue - } - val audioTracks = - playerCaptionsTracklistRenderer.getJSONArray("audioTracks") - if (audioTracks.length() < 2) { - continue - } - if (!json.has("streamingData")) { - continue - } - val streamingData = - json.getJSONObject("streamingData") - if (!streamingData.has("adaptiveFormats")) { - continue - } - val adaptiveFormats = - streamingData.getJSONArray("adaptiveFormats") - if (adaptiveFormats.length() < 2) { - continue - } - for (i in adaptiveFormats.length() - 1 downTo 0) { - val adaptiveFormat = adaptiveFormats.getJSONObject(i) + if (!json.has("captions")) { + continue + } + val captions = + json.getJSONObject("captions") + if (!captions.has("playerCaptionsTracklistRenderer")) { + continue + } + val playerCaptionsTracklistRenderer = + captions.getJSONObject("playerCaptionsTracklistRenderer") + if (!playerCaptionsTracklistRenderer.has("audioTracks")) { + continue + } + val audioTracks = + playerCaptionsTracklistRenderer.getJSONArray("audioTracks") + if (audioTracks.length() < 2) { + continue + } + if (!json.has("streamingData")) { + continue + } + val streamingData = + json.getJSONObject("streamingData") + if (!streamingData.has("adaptiveFormats")) { + continue + } + val adaptiveFormats = + streamingData.getJSONArray("adaptiveFormats") + if (adaptiveFormats.length() < 2) { + continue + } + for (i in adaptiveFormats.length() - 1 downTo 0) { + val adaptiveFormat = adaptiveFormats.getJSONObject(i) - if (adaptiveFormat.has("audioTrack")) { - val audioTrack = adaptiveFormat.getJSONObject("audioTrack") - val displayName = audioTrack.getString("displayName") - if (StringUtils.contains(displayName, "original")) { - val id = audioTrack.getString("id") - val dotIndex = StringUtils.indexOf(id, ".") - if (dotIndex > -1) { - val languageCode = id.substring(0, dotIndex) - overrideLanguage = languageCode - return + if (adaptiveFormat.has("audioTrack")) { + val audioTrack = adaptiveFormat.getJSONObject("audioTrack") + val displayName = audioTrack.getString("displayName") + if (StringUtils.contains(displayName, "original")) { + val id = audioTrack.getString("id") + val dotIndex = StringUtils.indexOf(id, ".") + if (dotIndex > -1) { + val languageCode = id.substring(0, dotIndex) + overrideLanguage = languageCode + return + } } } } + } catch (ex: SocketTimeoutException) { + handleConnectionError("Connection timeout", ex) + } catch (ex: IOException) { + handleConnectionError("Network error", ex) + } catch (ex: Exception) { + Logger.printException({ "send failed" }, ex) + } finally { + Logger.printDebug { "Fetching audio track request end (videoId: $videoId, took: ${(System.currentTimeMillis() - startTime)} ms)" } } - } catch (ex: SocketTimeoutException) { - handleConnectionError("Connection timeout", ex) - } catch (ex: IOException) { - handleConnectionError("Network error", ex) - } catch (ex: Exception) { - Logger.printException({ "send failed" }, ex) - } finally { - Logger.printDebug { "Fetching audio track request end (videoId: $videoId, took: ${(System.currentTimeMillis() - startTime)} ms)" } } } if (overrideLanguage.isEmpty()) { @@ -551,7 +547,11 @@ class StreamingDataRequest private constructor( if (!clientType.supportsCookies && BaseSettings.DISABLE_AUTO_AUDIO_TRACKS.get() && overrideLanguage.isEmpty()) { - setAudioTrackLanguage(videoId, requestHeader) + setAudioTrackLanguage( + videoId, + requestHeader, + clientType == ClientType.MWEB + ) } val connection = send( diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseHostActivity.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseHostActivity.java index a48884cb1..42f5d9319 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseHostActivity.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseHostActivity.java @@ -178,5 +178,5 @@ public abstract class BaseHostActivity extends Activity { */ protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {} - protected abstract boolean handleFinish(); + protected abstract boolean handleBackPress(); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java index cbbeddeea..152aef2f6 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java @@ -9,6 +9,7 @@ import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; @@ -646,6 +647,13 @@ public class PlayerPatch { return Settings.HIDE_SEEK_MESSAGE.get(); } + @Nullable + public static ViewStub hideSeekMessage(ViewStub viewStub) { + return Settings.HIDE_SEEK_MESSAGE.get() + ? null + : viewStub; + } + public static boolean hideSeekUndoMessage() { return Settings.HIDE_SEEK_UNDO_MESSAGE.get(); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java index a904ef349..d3ff1e8ad 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java @@ -74,6 +74,11 @@ public class SwipeControlsPatch { return Settings.SWIPE_SPEED.get() && !Settings.SWIPE_OVERLAY_STYLE.get().isLegacy(); } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_SPEED, Settings.SWIPE_OVERLAY_STYLE); + } } public static final class SwipeOverlaySeekColorAvailability implements Setting.Availability { @@ -82,5 +87,10 @@ public class SwipeControlsPatch { return Settings.SWIPE_SEEK.get() && !Settings.SWIPE_OVERLAY_STYLE.get().isLegacy(); } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_SEEK, Settings.SWIPE_OVERLAY_STYLE); + } } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 9a0e877d1..65a3dbbf2 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -741,6 +741,18 @@ public class Settings extends BaseSettings { public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false); public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false); + // Deprecated migrations + private static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f, false, false); + private static final FloatSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f, false, false); + static { // region Migration initialized @@ -798,6 +810,19 @@ public class Settings extends BaseSettings { migrateFromOldPreferences(ytPrefs, setting, key); } } + + // Migrate old saved data. Must be done here before the settings can be used by any other code. + applyOldSbOpacityToColor(SB_CATEGORY_SPONSOR_COLOR, DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_SELF_PROMO_COLOR, DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_INTERACTION_COLOR, DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_HIGHLIGHT_COLOR, DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_HOOK_COLOR, DEPRECATED_SB_CATEGORY_HOOK_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_INTRO_COLOR, DEPRECATED_SB_CATEGORY_INTRO_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_OUTRO_COLOR, DEPRECATED_SB_CATEGORY_OUTRO_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_PREVIEW_COLOR, DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_FILLER_COLOR, DEPRECATED_SB_CATEGORY_FILLER_OPACITY); + applyOldSbOpacityToColor(SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY); + // endregion // region SB import/export callbacks @@ -806,4 +831,13 @@ public class Settings extends BaseSettings { // endregion } + + private static void applyOldSbOpacityToColor(StringSetting colorSetting, FloatSetting opacitySetting) { + String colorString = colorSetting.get(); + if (colorString.length() >= 8) { + return; // Color is already #ARGB + } + + colorSetting.save(SponsorBlockSettings.migrateOldColorString(colorString, opacitySetting.get())); + } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/WebViewHostActivity.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/WebViewHostActivity.java index 1c2aa5ecc..a1ce16401 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/WebViewHostActivity.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/WebViewHostActivity.java @@ -31,6 +31,7 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toolbar; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -141,7 +142,12 @@ public class WebViewHostActivity extends Activity { WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS); } } - + } + if (isSDKAbove(33)) { + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + this::finish + ); } webView = findViewById(getIdIdentifier("revanced_webview")); @@ -207,7 +213,7 @@ public class WebViewHostActivity extends Activity { toolbar = new Toolbar(toolBarParent.getContext()); toolbar.setBackgroundColor(Color.WHITE); toolbar.setNavigationIcon(ThemeUtils.getBackButtonDrawable(false)); - toolbar.setNavigationOnClickListener(view -> this.onBackPressed()); + toolbar.setNavigationOnClickListener(view -> this.finish()); toolbar.setTitle(toolbarLabel); final int margin = Utils.dipToPixels(16); toolbar.setTitleMarginStart(margin); diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java index 17cbf5465..7c548d157 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java @@ -144,14 +144,10 @@ public class YouTubeActivityHook extends BaseActivityHook { } /** - * Injection point. - *

- * Overrides {@link Activity#finish()} of the injection Activity. - * - * @return if the original activity finish method should be allowed to run. + * @return Whether pressing the back button should be allowed. */ @SuppressWarnings("unused") - public static boolean handleFinish() { - return YouTubeSearchViewController.handleFinish(searchViewController); + public static boolean handleBackPress() { + return YouTubeSearchViewController.handleBackPress(searchViewController); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeHostActivity.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeHostActivity.java index dd91425bb..173a90e5d 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeHostActivity.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/YouTubeHostActivity.java @@ -28,9 +28,9 @@ public class YouTubeHostActivity extends BaseHostActivity { } @Override - public void finish() { - if (!handleFinish()) { - super.finish(); + public void onBackPressed() { + if (handleBackPress()) { + super.onBackPressed(); } } @@ -98,14 +98,10 @@ public class YouTubeHostActivity extends BaseHostActivity { } /** - * Injection point. - *

- * Overrides {@link Activity#finish()} of the injection Activity. - * - * @return if the original activity finish method should be allowed to run. + * @return Whether pressing the back button should be allowed. */ @Override - protected boolean handleFinish() { - return YouTubeSearchViewController.handleFinish(searchViewController); + protected boolean handleBackPress() { + return YouTubeSearchViewController.handleBackPress(searchViewController); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java index b979994ea..7a0773bc0 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java @@ -41,13 +41,12 @@ public class YouTubeSearchViewController extends BaseSearchViewController { protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) { } - // Static method for Activity finish. - public static boolean handleFinish(YouTubeSearchViewController searchViewController) { + public static boolean handleBackPress(YouTubeSearchViewController searchViewController) { if (searchViewController != null && searchViewController.isSearchActive()) { searchViewController.closeSearch(); - return true; + return false; } - return false; + return true; } // Adapter to wrap YouTubePreferenceFragment to BasePreferenceFragment interface. diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java index aec746aa6..5d2c5bc69 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -14,6 +14,7 @@ import androidx.annotation.Nullable; import org.json.JSONArray; import org.json.JSONObject; +import java.util.Locale; import java.util.UUID; import app.revanced.extension.shared.settings.Setting; @@ -261,6 +262,22 @@ public class SponsorBlockSettings { return uuid; } + public static String migrateOldColorString(String colorString, float opacity) { + if (colorString.length() >= 8) { + return colorString; + } + + // Change color string from #RGB to #ARGB using default alpha. + if (colorString.startsWith("#")) { + colorString = colorString.substring(1); + } + + String alphaHex = String.format(Locale.US, "%02X", (int)(opacity * 255)); + String argbColorString = '#' + alphaHex + colorString.substring(0, 6); + Logger.printDebug(() -> "Migrating old color string with default opacity: " + argbColorString); + return argbColorString; + } + private static boolean initialized; public static void initialize() { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java index 76607ea10..d8a55a91a 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java @@ -2,6 +2,7 @@ package app.revanced.extension.youtube.sponsorblock.objects; import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.Utils.dipToPixels; +import static app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings.migrateOldColorString; import android.content.Context; import android.view.View; @@ -54,6 +55,10 @@ public class SegmentCategoryPreference extends ColorPickerPreference { @Override public final void setText(String colorString) { try { + // Migrate old data imported in the settings UI. + // This migration is needed here because pasting into the settings + // immediately syncs the data with the preferences. + colorString = migrateOldColorString(colorString, SegmentCategory.CATEGORY_DEFAULT_OPACITY); super.setText(colorString); // Save to category. @@ -68,7 +73,8 @@ public class SegmentCategoryPreference extends ColorPickerPreference { Utils.showToastShort(str("revanced_settings_color_invalid")); setText(category.colorSetting.defaultValue); } catch (Exception ex) { - Logger.printException(() -> "setText failure: " + colorString, ex); + String colorStringFinal = colorString; + Logger.printException(() -> "setText failure: " + colorStringFinal, ex); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt index dfb642e06..ec45f0926 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt @@ -1,8 +1,6 @@ package app.revanced.extension.youtube.swipecontrols.controller.gesture import android.view.MotionEvent -import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserver -import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserverImpl import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController @@ -18,8 +16,7 @@ import app.revanced.extension.youtube.swipecontrols.misc.toPoint class ClassicSwipeController( private val controller: SwipeControlsHostActivity, private val config: SwipeControlsConfigurationProvider, -) : BaseGestureController(controller), - PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller) { +) : BaseGestureController(controller) { /** * the last event captured in [onDown] */ @@ -29,10 +26,6 @@ class ClassicSwipeController( get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL override fun isInSwipeZone(motionEvent: MotionEvent): Boolean { - // Fix https://github.com/inotia00/ReVanced_Extended/issues/3052 - if (!isFullscreenEngagementPanelVisible) { - return false - } val inVolumeZone = if (controller.config.enableVolumeControls) { (motionEvent.toPoint() in controller.zones.volume) } else { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt index cf3f10377..08759ca04 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt @@ -2,6 +2,8 @@ package app.revanced.extension.youtube.swipecontrols.controller.gesture.core import android.view.GestureDetector import android.view.MotionEvent +import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserver +import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserverImpl import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity /** @@ -17,6 +19,7 @@ abstract class BaseGestureController( SwipeDetector by SwipeDetectorImpl( controller.config.swipeMagnitudeThreshold.toDouble(), ), + PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller), VolumeAndBrightnessScroller by VolumeAndBrightnessScrollerImpl( controller.audio, controller.screen, @@ -44,6 +47,10 @@ abstract class BaseGestureController( if (!controller.config.enableSwipeControls) { return false } + // Fix https://github.com/inotia00/ReVanced_Extended/issues/3052 + if (isFullscreenEngagementPanelVisible) { + return false + } // create a copy of the event so we can modify it // without causing any issues downstream diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/Cache.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/Cache.kt new file mode 100644 index 000000000..ff858690b --- /dev/null +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/Cache.kt @@ -0,0 +1,11 @@ +package com.liskovsoft.youtubeapi.app.nsigsolver.common + +internal class Cache { + fun load(section: String, key: String): String? { + return null + } + + fun store(section: String, key: String, content: String) { + + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/InfoExtractor.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/InfoExtractor.kt index 6122ebb6b..09563a55c 100644 --- a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/InfoExtractor.kt +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/InfoExtractor.kt @@ -2,17 +2,19 @@ package com.liskovsoft.youtubeapi.app.nsigsolver.common import com.liskovsoft.googlecommon.common.api.FileApi import com.liskovsoft.googlecommon.common.helpers.RetrofitHelper +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.IEContentProviderError import kotlinx.coroutines.delay internal abstract class InfoExtractor { private val fileApi = RetrofitHelper.create(FileApi::class.java) - - protected suspend fun downloadWebpage(url: String, tries: Int = 1, timeoutMs: Long = 1_000): String? { + + protected suspend fun downloadWebpage(url: String, tries: Int = 1, timeoutMs: Long = 1_000, errorMsg: String? = null): String { var tryCount = 0 while (true) { try { - return RetrofitHelper.get(fileApi.getContent(url))?.content + val content = RetrofitHelper.get(fileApi.getContent(url))?.content + return content ?: throw IEContentProviderError(errorMsg ?: "Empty content received for the $url") } catch (e: Exception) { tryCount++ if (tryCount >= tries) diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/YTInfoExtractor.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/YTInfoExtractor.kt index 7659acda5..d4f280f35 100644 --- a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/YTInfoExtractor.kt +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/common/YTInfoExtractor.kt @@ -3,12 +3,14 @@ package com.liskovsoft.youtubeapi.app.nsigsolver.common import kotlinx.coroutines.runBlocking internal object YTInfoExtractor: InfoExtractor() { - // TODO: implement caching to the file - fun loadPlayer(playerUrl: String): String? = runBlocking { + val cache: Cache = Cache() + + // TODO: implement caching to the local storage + fun loadPlayer(playerUrl: String): String = runBlocking { return@runBlocking downloadWebpage(playerUrl) } - fun downloadWebpageWithRetries(url: String) = runBlocking { - return@runBlocking downloadWebpage(url, tries = 3) + fun downloadWebpageWithRetries(url: String, errorMsg: String? = null): String = runBlocking { + return@runBlocking downloadWebpage(url, tries = 3, errorMsg = errorMsg) } } \ No newline at end of file diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/JsChallengeProvider.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/JsChallengeProvider.kt index 27b2dde45..1b2d105e5 100644 --- a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/JsChallengeProvider.kt +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/JsChallengeProvider.kt @@ -3,6 +3,7 @@ package com.liskovsoft.youtubeapi.app.nsigsolver.provider import com.liskovsoft.youtubeapi.app.nsigsolver.common.YTInfoExtractor internal abstract class JsChallengeProvider { + protected val ie = YTInfoExtractor protected abstract val supportedTypes: List private fun validateRequest(request: JsChallengeRequest) { @@ -31,11 +32,11 @@ internal abstract class JsChallengeProvider { /** * Subclasses can override this method to handle bulk solving */ - protected abstract fun realBulkSolve(validatedRequests: List): Sequence + protected abstract fun realBulkSolve(requests: List): Sequence - protected fun getPlayer(videoId: String, playerUrl: String): String? { + protected fun getPlayer(videoId: String?, playerUrl: String): String { return try { - YTInfoExtractor.loadPlayer(playerUrl) + ie.loadPlayer(playerUrl) } catch (e: Exception) { throw JsChallengeProviderError("Failed to load player for JS challenge: $playerUrl", e) } diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/ProviderData.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/ProviderData.kt index d218b2246..b5ef1ecd5 100644 --- a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/ProviderData.kt +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/provider/ProviderData.kt @@ -13,11 +13,11 @@ internal data class JsChallengeRequest( internal data class ChallengeInput( val playerUrl: String, - val challenges: MutableList = mutableListOf() + val challenges: List ) internal data class ChallengeOutput( - val results: MutableMap = mutableMapOf() + val results: Map ) internal data class JsChallengeProviderResponse( diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsRuntimeChalBaseJCP.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsRuntimeChalBaseJCP.kt index 40eab0e21..ee158d5df 100644 --- a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsRuntimeChalBaseJCP.kt +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsRuntimeChalBaseJCP.kt @@ -1,12 +1,156 @@ package com.liskovsoft.youtubeapi.app.nsigsolver.runtime +import app.revanced.extension.shared.utils.Logger +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.ChallengeOutput import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeProvider +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeProviderError +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeProviderRejectedRequest +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeProviderResponse +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeRequest +import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeResponse import com.liskovsoft.youtubeapi.app.nsigsolver.provider.JsChallengeType internal abstract class JsRuntimeChalBaseJCP: JsChallengeProvider() { + private val tag = JsRuntimeChalBaseJCP::class.simpleName + private val cacheSection = "challenge-solver" + private val jcpGuideUrl = "https://github.com/yt-dlp/yt-dlp/wiki/YouTube-JS-Challenges" private val repository = "yt-dlp/yt-dlp-jsc-deno" + private val supportedVersion = "0.0.1" + override val supportedTypes = listOf(JsChallengeType.N, JsChallengeType.SIG) - + private val scriptFilenames = mapOf( + ScriptType.LIB to "lib.js", + ScriptType.CORE to "core.js" + ) + + private val minScriptFilenames = mapOf( + ScriptType.LIB to "lib.min.js", + ScriptType.CORE to "core.min.js" + ) + + abstract fun runJsRuntime(stdin: String): String + + override fun realBulkSolve(requests: List): Sequence = sequence { + val grouped: Map> = requests.groupBy { it.input.playerUrl } + + for ((playerUrl, groupedRequests) in grouped) { + var player = ie.cache.load(cacheSection, "player:$playerUrl") + + val cached = if (player != null) { + true + } else { + val videoId = groupedRequests.firstOrNull()?.videoId + player = getPlayer(videoId, playerUrl) + false + } + + val stdin = constructStdin(player, cached, groupedRequests) + val stdout = runJsRuntime(stdin) + + val gson = Gson() + val output: SolverOutput = try { + gson.fromJson(stdout, solverOutputType) + } catch (e: JsonSyntaxException) { + throw JsChallengeProviderError("Cannot parse solver output", e) + } + + if (output.type == "error") + throw JsChallengeProviderError(output.error ?: "Unknown solver output error") + + val preprocessed = output.preprocessed_player + if (preprocessed != null) + ie.cache.store(cacheSection, "player:$playerUrl", preprocessed) + + for ((request, responseData) in groupedRequests.zip(output.responses)) { + if (responseData.type == "error") { + yield(JsChallengeProviderResponse( + request, null, JsChallengeProviderError(responseData.error ?: "Unknown solver output error"))) + } else { + yield(JsChallengeProviderResponse( + request, JsChallengeResponse(request.type, ChallengeOutput(responseData.data)) + )) + } + } + } + } + + private fun constructStdin(player: String, preprocessed: Boolean, requests: List): String { + val jsonRequests = requests.map { request -> + mapOf( + // TODO: i despise nsig name + "type" to if (request.type.value == "n") "nsig" else request.type.value, + "challenges" to request.input.challenges + ) + } + val data = if (preprocessed) { + mapOf( + "type" to "preprocessed", + "preprocessed_player" to player, + "requests" to jsonRequests + ) + } else { + mapOf( + "type" to "player", + "player" to player, + "requests" to jsonRequests, + "output_preprocessed" to true + ) + } + val gson = Gson() + val jsonData = gson.toJson(data) + return """ + ${libScript.code} + const { astring, meriyah } = lib; + ${coreScript.code} + console.log(JSON.stringify(jsc($jsonData))); + """ + } + + // region: challenge solver script + + private val libScript: Script by lazy { + getScript(ScriptType.LIB) + } + + private val coreScript: Script by lazy { + getScript(ScriptType.CORE) + } + + private fun getScript(scriptType: ScriptType): Script { + for ((_, fromSource) in iterScriptSources()) { + val script = fromSource(scriptType) + if (script == null) + continue + if (script.version != supportedVersion) + Logger.printDebug { "Challenge solver ${scriptType.value} script version ${script.version} " + + "is not supported (source: ${script.source.value}, supported version: $supportedVersion)" } + + Logger.printDebug { "Using challenge solver ${script.type.value} script v${script.version} " + + "(source: ${script.source.value}, variant: ${script.variant.value})" } + return script + } + throw JsChallengeProviderRejectedRequest("No usable challenge solver ${scriptType.value} script available") + } + + private fun iterScriptSources(): Sequence Script?>> = sequence { + yieldAll(listOf( + //Pair(ScriptSource.CACHE, cachedSource), + //Pair(ScriptSource.BUILTIN, builtinSource), + Pair(ScriptSource.WEB, ::webReleaseSource) + )) + } + + private fun webReleaseSource(scriptType: ScriptType): Script? { + val url = "https://github.com/$repository/releases/download/$supportedVersion/${minScriptFilenames[scriptType]}" + val code = ie.downloadWebpageWithRetries(url, "[${tag}] Failed to download challenge solver ${scriptType.value} script") + Logger.printDebug { "[${tag}] Downloading challenge solver ${scriptType.value} script from $url" } + ie.cache.store(cacheSection, scriptType.value, code) + return Script(scriptType, ScriptVariant.MINIFIED, ScriptSource.WEB, supportedVersion, code) + } + + // endregion: challenge solver script } \ No newline at end of file diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsonData.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsonData.kt new file mode 100644 index 000000000..b423b53d7 --- /dev/null +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/JsonData.kt @@ -0,0 +1,18 @@ +package com.liskovsoft.youtubeapi.app.nsigsolver.runtime + +import com.google.gson.reflect.TypeToken + +internal val solverOutputType = object : TypeToken() {}.type + +internal data class SolverOutput( + val type: String, + val error: String?, + val preprocessed_player: String?, + val responses: List +) + +internal data class ResponseData( + val type: String, + val error: String?, + val data: Map +) \ No newline at end of file diff --git a/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/RuntimeData.kt b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/RuntimeData.kt new file mode 100644 index 000000000..1f4b1d428 --- /dev/null +++ b/extensions/shared/src/main/java/com/liskovsoft/youtubeapi/app/nsigsolver/runtime/RuntimeData.kt @@ -0,0 +1,42 @@ +package com.liskovsoft.youtubeapi.app.nsigsolver.runtime + +import java.security.MessageDigest + +internal enum class ScriptType(val value: String) { + LIB("lib"), + CORE("core"); +} + +internal enum class ScriptVariant(val value: String) { + UNKNOWN("unknown"), + MINIFIED("minified"), + UNMINIFIED("unminified"), + DENO_NPM("deno_npm"), + BUN_NPM("bun_npm"); +} + +internal enum class ScriptSource(val value: String) { + PYPACKAGE("python package"), + BINARY("binary"), + CACHE("cache"), + WEB("web"), + BUILTIN("builtin"); +} + +internal data class Script( + val type: ScriptType, + val variant: ScriptVariant, + val source: ScriptSource, + val version: String, + val code: String, +) { + val hash: String by lazy { + val digest = MessageDigest.getInstance("SHA3-512") + val bytes = digest.digest(code.toByteArray(Charsets.UTF_8)) + bytes.joinToString("") { "%02x".format(it) } + } + + override fun toString(): String { + return "