From d2dd60aa5c6391f70c369887de90254cd1ed0b6a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 8 Nov 2025 13:48:50 +0600 Subject: [PATCH] chore: update YoutubeExplode to v3 --- .../youtube_engine/quickjs_solver.dart | 167 ++++++++++++++++++ .../youtube_explode_engine.dart | 5 +- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 16 +- pubspec.yaml | 3 +- windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 lib/services/youtube_engine/quickjs_solver.dart diff --git a/lib/services/youtube_engine/quickjs_solver.dart b/lib/services/youtube_engine/quickjs_solver.dart new file mode 100644 index 00000000..4e8bfafb --- /dev/null +++ b/lib/services/youtube_engine/quickjs_solver.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:youtube_explode_dart/js_challenge.dart'; +// ignore: implementation_imports +import 'package:youtube_explode_dart/src/reverse_engineering/challenges/ejs/ejs.dart'; +import 'package:jsf/jsf.dart'; + +/// [WIP] +class QuickJSEJSSolver extends BaseJSChallengeSolver { + final _playerCache = {}; + final _sigCache = <(String, String, JSChallengeType), String>{}; + final QuickJSRuntime qjs; + QuickJSEJSSolver._(this.qjs); + + static Future init() async { + final modules = await EJSBuilder.getJSModules(); + final deno = await QuickJSRuntime.init(modules); + return QuickJSEJSSolver._(deno); + } + + @override + Future solve( + String playerUrl, JSChallengeType type, String challenge) async { + final key = (playerUrl, challenge, type); + if (_sigCache.containsKey(key)) { + return _sigCache[key]!; + } + + var playerScript = _playerCache[playerUrl]; + if (playerScript == null) { + final resp = await http.get(Uri.parse(playerUrl)); + playerScript = _playerCache[playerUrl] = resp.body; + } + final jsCall = EJSBuilder.buildJSCall(playerScript, { + type: [challenge], + }); + + final result = await qjs.eval(jsCall); + // Trim the first and last characters (' delimiters of the JS string) + final data = json.decode(result.substring(1, result.length - 1)) + as Map; + + if (data['type'] != 'result') { + throw Exception('Unexpected response type: ${data['type']}'); + } + final response = data['responses'][0]; + if (response['type'] != 'result') { + throw Exception('Unexpected item response type: ${response['type']}'); + } + final decoded = response['data'][challenge]; + if (decoded == null) { + throw Exception('No data for challenge: $challenge'); + } + + _sigCache[key] = decoded; + + return decoded; + } + + @override + void dispose() { + qjs.dispose(); + } +} + +class _EvalRequest { + final String code; + final Completer completer; + + _EvalRequest(this.code, this.completer); +} + +class QuickJSRuntime { + final JsRuntime _runtime; + final StreamController _stdoutController = + StreamController.broadcast(); + + // Queue for incoming eval requests + final Queue<_EvalRequest> _evalQueue = Queue<_EvalRequest>(); + bool _isProcessing = false; // Flag to indicate if an eval is currently active + + QuickJSRuntime(this._runtime); + + /// Disposes the Deno process. + void dispose() { + _stdoutController.close(); + _runtime.dispose(); + } + + /// Sends JavaScript code to Deno for evaluation. + /// Assumes single-line input produces single-line output. + Future eval(String code) { + final completer = Completer(); + final request = _EvalRequest(code, completer); + _evalQueue.addLast(request); // Add request to the end of the queue + _processQueue(); // Attempt to process the queue + + return completer.future; + } + + // Processes the eval queue. + void _processQueue() { + if (_isProcessing || _evalQueue.isEmpty) { + return; // Already processing or nothing in queue + } + + _isProcessing = true; + final request = + _evalQueue.first; // Get the next request without removing it yet + + StreamSubscription? currentOutputSubscription; + Completer lineReceived = Completer(); + + currentOutputSubscription = _stdoutController.stream.listen((data) { + if (!lineReceived.isCompleted) { + // Assuming single line output per eval. + // This will capture the first full line or chunk received after sending the code. + request.completer.complete(data.trim()); + lineReceived.complete(); + currentOutputSubscription + ?.cancel(); // Cancel subscription for this request + _evalQueue.removeFirst(); // Remove the processed request + _isProcessing = false; // Mark as no longer processing + _processQueue(); // Attempt to process next item in queue + } + }, onError: (e) { + if (!request.completer.isCompleted) { + request.completer.completeError(e); + lineReceived.completeError(e); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }, onDone: () { + if (!request.completer.isCompleted) { + request.completer.completeError( + StateError('Deno process closed while awaiting eval result.')); + lineReceived.completeError( + StateError('Deno process closed while awaiting eval result.')); + currentOutputSubscription?.cancel(); + _evalQueue.removeFirst(); + _isProcessing = false; + _processQueue(); + } + }); + + debugPrint("[QuickJS Solver] Evaluate ${request.code}"); + final result = _runtime.eval(request.code); + debugPrint("[QuickJS Solver] Evaluation Result $result"); + _stdoutController.add(result); + } + + static Future init(String initCode) async { + debugPrint("[QuickJS Solver] Initializing"); + debugPrint("[QuickJS Solver] script $initCode"); + + final runtime = JsRuntime(); + + runtime.execInitScript(initCode); + + return QuickJSRuntime(runtime); + } +} diff --git a/lib/services/youtube_engine/youtube_explode_engine.dart b/lib/services/youtube_engine/youtube_explode_engine.dart index c552f883..f8587ca6 100644 --- a/lib/services/youtube_engine/youtube_explode_engine.dart +++ b/lib/services/youtube_engine/youtube_explode_engine.dart @@ -2,6 +2,7 @@ import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:spotube/services/youtube_engine/youtube_engine.dart'; +// import 'package:youtube_explode_dart/solvers.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'dart:async'; @@ -55,8 +56,9 @@ class IsolatedYoutubeExplode { } } - static void _isolateEntry(SendPort mainSendPort) { + static Future _isolateEntry(SendPort mainSendPort) async { final receivePort = ReceivePort(); + // final solver = await DenoEJSSolver.init(); final youtubeExplode = YoutubeExplode(); final stopWatch = kDebugMode ? Stopwatch() : null; @@ -163,6 +165,7 @@ class YouTubeExplodeEngine implements YouTubeEngine { ytClients: [ YoutubeApiClient.ios, YoutubeApiClient.androidVr, + YoutubeApiClient.android, ], ); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e5c8a845..541826e6 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -24,6 +24,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god ) diff --git a/pubspec.lock b/pubspec.lock index d86cb541..7e53e91c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -446,10 +446,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -1439,6 +1439,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + jsf: + dependency: "direct main" + description: + name: jsf + sha256: "189ba3b9216702f9b6f2d8ea90fa5acaca13bbe5dd2f72fb38618005b41a737f" + url: "https://pub.dev" + source: hosted + version: "0.6.1" json_annotation: dependency: "direct main" description: @@ -2840,10 +2848,10 @@ packages: dependency: "direct main" description: name: youtube_explode_dart - sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0" + sha256: add33de45d80c7f71a5e3dd464dd82fafd7fb5ab875fd303c023f30f76618325 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "3.0.0" yt_dlp_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fd3d78c5..9b84196b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -131,7 +131,7 @@ dependencies: wikipedia_api: ^0.1.0 win32_registry: ^1.1.5 window_manager: ^0.4.3 - youtube_explode_dart: ^2.5.3 + youtube_explode_dart: ^3.0.0 yt_dlp_dart: git: url: https://github.com/KRTirtho/yt_dlp_dart.git @@ -161,6 +161,7 @@ dependencies: flutter_markdown_plus: ^1.0.3 pub_semver: ^2.2.0 change_case: ^1.1.0 + jsf: ^0.6.1 dev_dependencies: build_runner: ^2.4.13 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6e831cf5..53cd3667 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -27,6 +27,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_discord_rpc + jsf metadata_god smtc_windows )