mangayomi-mirror/lib/services/fetch_sources_list.dart
2025-10-10 15:49:33 +02:00

523 lines
16 KiB
Dart

import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/eval/model/filter.dart';
import 'package:mangayomi/eval/model/source_preference.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:package_info_plus/package_info_plus.dart';
Future<void> fetchSourcesList({
int? id,
required bool refresh,
required Ref ref,
required ItemType itemType,
required Repo? repo,
}) async {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
final url = repo?.jsonUrl;
if (url == null) return;
final req = await http.get(Uri.parse(url));
final info = await PackageInfo.fromPlatform();
final sourceList = (jsonDecode(req.body) as List)
.expand((e) sync* {
if (e['name'] != null &&
e['pkg'] != null &&
e['version'] != null &&
e['code'] != null &&
e['lang'] != null &&
e['nsfw'] != null &&
e['sources'] != null &&
e['apk'] != null) {
final repoUrl = url.replaceAll("/index.min.json", "");
final sources = e['sources'] as List;
for (final source in sources) {
final src = Source.fromJson(e)
..apiUrl = ''
..appMinVerReq = ''
..dateFormat = ''
..dateFormatLocale = ''
..hasCloudflare = false
..headers = ''
..isActive = true
..isAdded = false
..isFullData = false
..isNsfw = e['nsfw'] == 1
..isPinned = false
..lastUsed = false
..sourceCode = ''
..typeSource = ''
..versionLast = '0.0.1'
..isObsolete = false
..isLocal = false
..name = source['name']
..lang = source['lang']
..baseUrl = source['baseUrl']
..sourceCodeUrl = "$repoUrl/apk/${e['apk']}"
..sourceCodeLanguage = SourceCodeLanguage.mihon
..itemType =
(e['pkg'] as String).startsWith(
"eu.kanade.tachiyomi.animeextension",
)
? ItemType.anime
: ItemType.manga
..iconUrl = "$repoUrl/icon/${e['pkg']}.png"
..notes =
"Requires Android Proxy Server (ApkBridge) for installing and using the extensions!";
src.id = 'mihon-${source['id']}'.hashCode;
yield src;
}
} else if (e['id'] is String &&
e['name'] != null &&
e['site'] != null &&
e['lang'] != null &&
e['version'] != null &&
e['url'] != null &&
e['iconUrl'] != null) {
final src = Source.fromJson(e)
..apiUrl = ''
..appMinVerReq = ''
..dateFormat = ''
..dateFormatLocale = ''
..hasCloudflare = false
..headers = ''
..isActive = true
..isAdded = false
..isFullData = false
..isNsfw = false
..isPinned = false
..lastUsed = false
..sourceCode = ''
..typeSource = ''
..versionLast = '0.0.1'
..isObsolete = false
..isLocal = false
..lang = _convertLang(e)
..baseUrl = e['site']
..sourceCodeUrl = e['url']
..sourceCodeLanguage = SourceCodeLanguage.lnreader
..itemType = ItemType.novel
..notes = "Performance might be poor due to limited engine";
src.id = 'lnreader-plugin-"${src.name}"."${src.lang}"'.hashCode;
yield src;
} else {
yield Source.fromJson(e);
}
})
.where(
(source) =>
source.itemType == itemType &&
source.appMinVerReq != null &&
compareVersions(info.version, source.appMinVerReq!) > -1,
)
.toList();
if (id != null) {
final matchingSource = sourceList.firstWhere(
(source) => source.id == id,
orElse: () => Source(),
);
if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) {
await _updateSource(matchingSource, ref, repo, itemType);
}
} else {
for (var source in sourceList) {
final existingSource = await isar.sources.get(source.id!);
if (existingSource == null) {
await _addNewSource(source, ref, repo, itemType);
continue;
}
final shouldUpdate =
existingSource.isAdded! &&
compareVersions(existingSource.version!, source.version!) < 0;
if (!shouldUpdate) continue;
if (ref.read(autoUpdateExtensionsStateProvider)) {
await _updateSource(source, ref, repo, itemType);
} else {
await isar.writeTxn(() async {
isar.sources.put(existingSource..versionLast = source.version);
});
}
}
}
checkIfSourceIsObsolete(sourceList, repo!, itemType, ref);
}
Future<void> _updateSource(
Source source,
Ref ref,
Repo? repo,
ItemType itemType,
) async {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
final req = await http.get(Uri.parse(source.sourceCodeUrl!));
final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon
? base64.encode(req.bodyBytes)
: req.body;
final androidProxyServer = ref.read(androidProxyServerStateProvider);
Map<String, String> headers = {};
bool? supportLatest;
FilterList? filterList;
List<SourcePreference>? preferenceList;
if (source.sourceCodeLanguage == SourceCodeLanguage.mihon) {
headers = await fetchHeadersDalvik(
http,
source..sourceCode = sourceCode,
androidProxyServer,
);
supportLatest = await fetchSupportLatestDalvik(
http,
source..sourceCode = sourceCode,
androidProxyServer,
);
filterList = await fetchFilterListDalvik(
http,
source..sourceCode = sourceCode,
androidProxyServer,
);
preferenceList = await fetchPreferencesDalvik(
http,
source..sourceCode = sourceCode,
androidProxyServer,
);
} else {
headers = getExtensionService(
source..sourceCode = sourceCode,
androidProxyServer,
).getHeaders();
}
final updatedSource = Source()
..headers = jsonEncode(headers)
..supportLatest = supportLatest
..filterList = filterList != null ? jsonEncode(filterList.toJson()) : null
..preferenceList = preferenceList != null
? jsonEncode(preferenceList.map((e) => e.toJson()).toList())
: null
..isAdded = true
..sourceCode = sourceCode
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
..apiUrl = source.apiUrl
..baseUrl = source.baseUrl
..dateFormat = source.dateFormat
..dateFormatLocale = source.dateFormatLocale
..hasCloudflare = source.hasCloudflare
..iconUrl = source.iconUrl
..typeSource = source.typeSource
..lang = source.lang
..isNsfw = source.isNsfw
..name = source.name
..version = source.version
..versionLast = source.version
..itemType = itemType
..isFullData = source.isFullData ?? false
..appMinVerReq = source.appMinVerReq
..sourceCodeLanguage = source.sourceCodeLanguage
..additionalParams = source.additionalParams ?? ""
..isObsolete = false
..notes = source.notes
..repo = repo
..updatedAt = DateTime.now().millisecondsSinceEpoch;
await isar.writeTxn(() async => isar.sources.put(updatedSource));
}
Future<void> _addNewSource(
Source source,
Ref ref,
Repo? repo,
ItemType itemType,
) async {
final newSource = Source()
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
..sourceCode = source.sourceCode
..apiUrl = source.apiUrl
..baseUrl = source.baseUrl
..dateFormat = source.dateFormat
..dateFormatLocale = source.dateFormatLocale
..hasCloudflare = source.hasCloudflare
..iconUrl = source.iconUrl
..typeSource = source.typeSource
..lang = source.lang
..isNsfw = source.isNsfw
..name = source.name
..version = source.version
..versionLast = source.version
..itemType = itemType
..sourceCodeLanguage = source.sourceCodeLanguage
..isFullData = source.isFullData ?? false
..appMinVerReq = source.appMinVerReq
..isObsolete = false
..notes = source.notes
..repo = repo
..updatedAt = DateTime.now().millisecondsSinceEpoch;
await isar.writeTxn(() async => isar.sources.put(newSource));
}
Future<void> checkIfSourceIsObsolete(
List<Source> sourceList,
Repo repo,
ItemType itemType,
Ref ref,
) async {
if (sourceList.isEmpty) return;
final sources = await isar.sources
.filter()
.idIsNotNull()
.itemTypeEqualTo(itemType)
.and()
.isLocalEqualTo(false)
.findAll();
if (sources.isEmpty) return;
final sourceIds = sourceList
.where((e) => e.id != null)
.map((e) => e.id!)
.toSet();
if (sourceIds.isEmpty) return;
final toUpdate = <Source>[];
for (var source in sources) {
final isNowObsolete =
!sourceIds.contains(source.id) && source.repo?.jsonUrl == repo.jsonUrl;
if (source.isObsolete != isNowObsolete) {
source.isObsolete = isNowObsolete;
source.updatedAt = DateTime.now().millisecondsSinceEpoch;
toUpdate.add(source);
}
}
if (toUpdate.isEmpty) return;
await isar.writeTxn(() => isar.sources.putAll(toUpdate));
}
int compareVersions(String version1, String version2) {
final v1Parts = version1.split('.');
final v2Parts = version2.split('.');
final minLength = v1Parts.length < v2Parts.length
? v1Parts.length
: v2Parts.length;
for (var i = 0; i < minLength; i++) {
final v1Value = int.parse(v1Parts[i].padRight(2, '0'));
final v2Value = int.parse(v2Parts[i].padRight(2, '0'));
final comparison = v1Value.compareTo(v2Value);
if (comparison != 0) return comparison;
}
return v1Parts.length.compareTo(v2Parts.length);
}
Future<Map<String, String>> fetchHeadersDalvik(
InterceptedClient client,
Source source,
String androidProxyServer,
) async {
try {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({"method": "headers$name", "data": source.sourceCode}),
);
final data = jsonDecode(res.body) as List;
final Map<String, String> headers = {};
for (var i = 0; i + 1 < data.length; i += 2) {
headers[data[i]] = data[i + 1];
}
return headers;
} catch (_) {
return {};
}
}
Future<bool> fetchSupportLatestDalvik(
InterceptedClient client,
Source source,
String androidProxyServer,
) async {
try {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "supportLatest$name",
"data": source.sourceCode,
}),
);
return res.body.trim() == "true";
} catch (_) {
return false;
}
}
Future<FilterList?> fetchFilterListDalvik(
InterceptedClient client,
Source source,
String androidProxyServer,
) async {
try {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({"method": "filters$name", "data": source.sourceCode}),
);
final data = jsonDecode(res.body) as List;
final filters = data.expand((e) sync* {
if (e['name'] is String &&
e['state'] is Map<String, dynamic> &&
e['values'] is List) {
yield SortFilter(
"${e['name']}Filter",
e['name'],
SortState(e['state']['index'], e['state']['ascending'], null),
(e['values'] as List)
.map((e) => SelectFilterOption(e, e, null))
.toList(),
null,
);
} else if (e['name'] is String &&
e['state'] is int &&
(e['values'] is List || e['vals'] is List)) {
yield SelectFilter(
"${e['name']}Filter",
e['name'],
e['state'],
e['vals'] is List
? (e['vals'] as List)
.map(
(e) => SelectFilterOption(e['first'], e['second'], null),
)
.toList()
: e['values'] is List
? (e['values'] as List)
.map((e) => SelectFilterOption(e, e, null))
.toList()
: [],
"SelectFilter",
);
} else if (e['name'] is String && e['state'] is List) {
yield GroupFilter(
"${e['name']}Filter",
e['name'],
(e['state'] as List).map((e) {
if (e['included'] is bool &&
e['ignored'] is bool &&
e['excluded'] is bool) {
return TriStateFilter(
null,
e['name'],
e['id'] ?? e['name'],
null,
state: e['state'],
);
}
return CheckBoxFilter(
null,
e['name'],
e['id'] ?? e['name'],
null,
state: e['state'],
);
}).toList(),
"GroupFilter",
);
} else if (e['name'] is String && e['state'] is String) {
yield TextFilter(
"${e['name']}Filter",
e['name'],
null,
state: e['state'],
);
} else if (e['name'] is String && e['state'] is int) {
yield HeaderFilter(e['name'], "${e['name']}Filter");
}
}).toList();
return FilterList(filters);
} catch (_) {
return null;
}
}
Future<List<SourcePreference>?> fetchPreferencesDalvik(
InterceptedClient client,
Source source,
String androidProxyServer,
) async {
try {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "preferences$name",
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as List;
return data
.map(
(e) => SourcePreference.fromJson(e)
..id = null
..sourceId = source.id,
)
.toList();
} catch (_) {
return null;
}
}
String _convertLang(dynamic e) {
final lang = e['lang'];
if (lang is String) {
switch (lang) {
case "‎العربية":
return "ar";
case "中文, 汉语, 漢語":
return "zh";
case "English":
return "en";
case "Français":
return "fr";
case "Bahasa Indonesia":
return "id";
case "日本語":
return "ja";
case "조선말, 한국어":
return "ko";
case "Polski":
return "pl";
case "Português":
return "pt";
case "Русский":
return "ru";
case "Español":
return "es";
case "ไทย":
return "th";
case "Türkçe":
return "tr";
case "Українська":
return "uk";
case "Tiếng Việt":
return "vi";
default:
return "all";
}
}
return "all";
}