Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client side script reload #598

Merged
merged 10 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading