Skip to content

Commit

Permalink
Merge pull request #598 from project-violet/client-side-script-reload
Browse files Browse the repository at this point in the history
Client side script reload
  • Loading branch information
violet-dev authored Dec 30, 2024
2 parents da43710 + 27bed8f commit 90dba6d
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 207 deletions.
2 changes: 1 addition & 1 deletion violet/lib/component/hitomi/hitomi_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class HitomiImageProvider extends VioletImageProvider {
return _heightCache![page];
}

final info = await ScriptManager.getGalleryInfo(id);
final info = await ScriptManager.getGalleryInfoRaw(id);
if (info == null) {
_heightCache = List<double>.filled(imageList.smallThumbnails!.length, -1);
return -1;
Expand Down
202 changes: 116 additions & 86 deletions violet/lib/script/script_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,129 +12,159 @@ import 'package:violet/log/log.dart';
import 'package:violet/network/wrapper.dart' as http;
import 'package:violet/script/freezed/script_model.dart';
import 'package:violet/script/script_webview.dart';
import 'package:violet/util/helper.dart';
import 'package:violet/widgets/article_item/image_provider_manager.dart';

class ScriptManager {
static const String _scriptNoCDNUrl =
static const String scriptNoCDNUrl =
'https://github.com/project-violet/scripts/blob/main/hitomi_get_image_list_v3.js';
static const String _scriptUrl =
static const String scriptUrl =
'https://raw.githubusercontent.com/project-violet/scripts/main/hitomi_get_image_list_v3.js';
static const String _scriptV4 =
static const String scriptV4Url =
'https://github.com/project-violet/scripts/raw/main/hitomi_get_image_list_v4_model.js';
static const String enableRefreshV4NoWebViewCheckUrl =
'https://raw.githubusercontent.com/project-violet/scripts/refs/heads/main/enableRefreshV4NoWebView';
static bool enableV4 = false;
static String? _v4Cache;
static String? _scriptCache;
static late JavascriptRuntime _runtime;
static late DateTime _latestUpdate;
static bool enableRefreshV4NoWebView = true;
static String? v4Cache;
static String? scriptCache;
static late JavascriptRuntime runtime;
static late DateTime latestUpdate;

static Future<void> init() async {
try {
final scriptHtml = (await http.get(_scriptNoCDNUrl)).body;
_scriptCache = json.decode(parse(scriptHtml)
.querySelector("script[data-target='react-app.embeddedData']")!
.text)['payload']['blob']['rawBlob'];
} catch (e, st) {
await Logger.warning('[ScriptManager-init] W: $e\n'
'$st');
debugPrint(e.toString());
}
if (_scriptCache == null) {
try {
_scriptCache = (await http.get(_scriptUrl)).body;
} catch (e, st) {
Future fallbackFail(Future Function() fn) async {
await catchUnwind(fn, (e, st) async {
await Logger.warning('[ScriptManager-init] W: $e\n'
'$st');
debugPrint(e.toString());
}
}
try {
_v4Cache = (await http.get(_scriptV4)).body;
} catch (e, st) {
await Logger.warning('[ScriptManager-init] W: $e\n'
'$st');
debugPrint(e.toString());
});
}
_latestUpdate = DateTime.now();
try {
_initRuntime();
} catch (e, st) {
await Logger.error('[ScriptManager-init] E: $e\n'
'$st');
debugPrint(e.toString());

await fallbackFail(() async {
final scriptHtml = (await http.get(scriptNoCDNUrl)).body;
scriptCache = json.decode(parse(scriptHtml)
.querySelector("script[data-target='react-app.embeddedData']")!
.text)['payload']['blob']['rawBlob'];
});

if (scriptCache == null) {
await fallbackFail(() async {
scriptCache = (await http.get(scriptUrl)).body;
});
}

await fallbackFail(() async {
v4Cache = (await http.get(scriptV4Url)).body;
});

await fallbackFail(() async {
final check = (await http.get(enableRefreshV4NoWebViewCheckUrl)).body;
enableRefreshV4NoWebView = int.parse(check) == 1;
if (enableRefreshV4NoWebView) {
await refreshV4NoWebView();
}
});

await fallbackFail(() async {
initRuntime();
});
}

static Future<bool> refresh() async {
if (enableV4) {
if (ScriptWebViewProxy.reload != null) {
ScriptWebViewProxy.reload!();
static Future<void> refresh() async {
// 1. (V4) NoWebView가 활성화되어 있다면 해당 방법으로 refresh 시도, 아니라면 webview로 시도
if (enableRefreshV4NoWebView) {
if (await refreshV4NoWebView()) {
return;
}
return false;
} else if (enableV4 && ScriptWebViewProxy.reload != null) {
/// proxy may be calling `refreshV4` function
ScriptWebViewProxy.reload!();
return;
}

if (DateTime.now().difference(_latestUpdate).inMinutes < 5) {
return false;
// 2. (V3) V4 disable 상태이거나 no web-view가 실패한다면 V3로 fallback한다
// 너무 잦은 refresh try를 방지하기 위해 많아도 5분에 한 번씩만 실행되게 끔 설정
if (DateTime.now().difference(latestUpdate).inMinutes < 5) {
return;
}

var scriptTemp = (await http.get(_scriptUrl)).body;
await refreshV3();
}

if (_scriptCache != scriptTemp) {
_scriptCache = scriptTemp;
_latestUpdate = DateTime.now();
_initRuntime();
ProviderManager.checkMustRefresh();
return true;
}
static Future<void> refreshV3() async {
final scriptTemp = (await http.get(scriptUrl)).body;
replaceScriptCacheIfRequired(scriptTemp);
}

return false;
static Future<bool> refreshV4NoWebView() async {
var success = false;
await catchUnwind(() async {
final ggBody = (await http.get('https://ltn.hitomi.la/gg.js')).body;
final ggRuntime = getJavascriptRuntime();
// TODO: 이유는 잘 모르겠으나 use strict를 삭제하지 않으면 gg instance를 찾을 수 없어서 실패함
ggRuntime.evaluate(ggBody.split("'use strict';")[1]);
final gg = ggRuntime.evaluate('''
var r = "";
for (var i = 0; i < 4096; i++) {
r += gg.m(i).toString();
r += ",";
}
r + '|' + gg.b
''').stringResult;
await refreshV4(gg.split('|')[0], gg.split('|')[1]);
success = true;
}, (e, st) async {
await Logger.warning('[ScriptManager-refreshV4NoWebView] W: $e\n'
'$st');
debugPrint(e.toString());
});
return success;
}

static Future<void> setV4(String ggM, String ggB) async {
/// this function may be called by `ScriptWebView`
static Future<void> refreshV4(String ggM, String ggB) async {
enableV4 = true;
v4Cache ??= (await http.get(scriptV4Url)).body;
final scriptTemp =
v4Cache!.replaceAll('%%gg.m%', ggM).replaceAll('%%gg.b%', ggB);
replaceScriptCacheIfRequired(scriptTemp);
}

_v4Cache ??= (await http.get(_scriptV4)).body;

var scriptTemp = _v4Cache!;
scriptTemp = scriptTemp.replaceAll('%%gg.m%', ggM);
scriptTemp = scriptTemp.replaceAll('%%gg.b%', ggB);

if (_scriptCache != scriptTemp) {
_scriptCache = scriptTemp;
_latestUpdate = DateTime.now();
_initRuntime();
ProviderManager.checkMustRefresh();
ViewerContext.signal((c) => c.refreshImgUrlWhenRequired());

Logger.info('[Script Manager] Update Sync!');
static void replaceScriptCacheIfRequired(String scriptTemp) {
if (scriptCache == scriptTemp) {
return;
}

scriptCache = scriptTemp;
initRuntime();
ProviderManager.checkMustRefresh();
ViewerContext.signal((c) => c.refreshImgUrlWhenRequired());
Logger.info('[Script Manager] Update Sync!');
}

static void _initRuntime() {
_runtime = getJavascriptRuntime();
_runtime.evaluate(_scriptCache!);
static void initRuntime() {
latestUpdate = DateTime.now();
runtime = getJavascriptRuntime();
runtime.evaluate(scriptCache!);
}

static Future<String?> getGalleryInfo(String id) async {
var downloadUrl =
_runtime.evaluate("create_download_url('$id')").stringResult;
var headers = await runHitomiGetHeaderContent(id);
var galleryInfo = await http.get(downloadUrl, headers: headers);
static Future<String?> getGalleryInfoRaw(String id) async {
final downloadUrl =
runtime.evaluate("create_download_url('$id')").stringResult;
final headers = await runHitomiGetHeaderContent(id.toString());
final galleryInfo = await http.get(downloadUrl, headers: headers);
if (galleryInfo.statusCode != 200) return null;
return galleryInfo.body;
}

static Future<ImageList?> runHitomiGetImageList(int id) async {
if (_scriptCache == null) return null;
if (scriptCache == null) return null;

try {
var downloadUrl =
_runtime.evaluate("create_download_url('$id')").stringResult;
var headers = await runHitomiGetHeaderContent(id.toString());
var galleryInfo = await http.get(downloadUrl,
headers: headers, timeout: const Duration(milliseconds: 1000));
if (galleryInfo.statusCode != 200) return null;
_runtime.evaluate(galleryInfo.body);
final jResult = _runtime.evaluate('hitomi_get_image_list()').stringResult;
final galleryInfoRaw = await getGalleryInfoRaw(id.toString());
if (galleryInfoRaw == null) return null;
runtime.evaluate(galleryInfoRaw);
final jResult = runtime.evaluate('hitomi_get_image_list()').stringResult;
final jResultImageList = ScriptImageList.fromJson(jsonDecode(jResult));

return ImageList(
Expand All @@ -152,10 +182,10 @@ class ScriptManager {

static Future<Map<String, String>> runHitomiGetHeaderContent(
String id) async {
if (_scriptCache == null) return <String, String>{};
if (scriptCache == null) return <String, String>{};
try {
final jResult =
_runtime.evaluate("hitomi_get_header_content('$id')").stringResult;
runtime.evaluate("hitomi_get_header_content('$id')").stringResult;
final jResultObject = jsonDecode(jResult);

if (jResultObject is Map<dynamic, dynamic>) {
Expand Down
2 changes: 1 addition & 1 deletion violet/lib/script/script_webview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,6 @@ class _ScriptWebViewState extends State<ScriptWebView>

print(ggB);

await ScriptManager.setV4(ggM!, ggB!);
await ScriptManager.refreshV4(ggM!, ggB!);
}
}
9 changes: 7 additions & 2 deletions violet/lib/util/helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
// Copyright (C) 2020-2024. violet-team. Licensed under the Apache-2.0 License.

// Suppress excepction
Future catchUnwind(Future Function() body) async {
Future catchUnwind(Future Function() body,
[Future Function(Object, Object)? err]) async {
try {
await body();
} catch (_) {}
} catch (e, st) {
if (err != null) {
await err(e, st);
}
}
}

Future fixedPoint<T>(Future Function() body, T Function() value) async {
Expand Down
Loading

0 comments on commit 90dba6d

Please sign in to comment.