adding support for Mihon extensions

This commit is contained in:
Schnitzel5 2025-08-20 03:59:38 +02:00
parent a15afd4334
commit 5f9efe957a
13 changed files with 1647 additions and 485 deletions

View file

@ -3,10 +3,12 @@ import 'package:mangayomi/models/source.dart';
import 'dart/service.dart';
import 'javascript/service.dart';
import 'mihon/service.dart';
ExtensionService getExtensionService(Source source) {
return switch (source.sourceCodeLanguage) {
SourceCodeLanguage.dart => DartExtensionService(source),
SourceCodeLanguage.javascript => JsExtensionService(source),
SourceCodeLanguage.mihon => MihonExtensionService(source, "http://localhost:8080"),
};
}

View file

@ -0,0 +1,85 @@
import 'package:mangayomi/models/manga.dart';
class MangaPages {
List<SManga> list;
bool hasNextPage;
MangaPages({required this.list, this.hasNextPage = false});
factory MangaPages.fromJson(Map<String, dynamic> json, ItemType itemType) {
final name = itemType == ItemType.anime ? "animes" : "mangas";
return MangaPages(
list: json[name] != null
? (json[name] as List).map((e) => SManga.fromJson(e)).toList()
: [],
hasNextPage: json['hasNextPage'],
);
}
Map<String, dynamic> toJson(ItemType itemType) => {
itemType == ItemType.anime ? "animes" : "mangas": list
.map((v) => v.toJson())
.toList(),
'hasNextPage': hasNextPage,
};
}
class SManga {
String? url;
String? title;
String? artist;
String? author;
String? description;
List<String>? genre;
Status? status;
String? thumbnailUrl;
SManga({
this.url,
this.title,
this.artist,
this.author,
this.description,
this.genre,
this.status = Status.unknown,
this.thumbnailUrl,
});
factory SManga.fromJson(Map<String, dynamic> json) {
return SManga(
url: json['url'],
title: json['title'],
artist: json['artist'],
author: json['author'],
description: json['description'],
genre: (json['genres'] as List?)?.map((e) => e.toString()).toList() ?? [],
status: switch (json['status'] as int?) {
1 => Status.ongoing,
2 => Status.completed,
4 => Status.publishingFinished,
5 => Status.canceled,
6 => Status.onHiatus,
_ => Status.unknown,
},
thumbnailUrl: json['thumbnail_url'],
);
}
Map<String, dynamic> toJson() {
return {
'url': url,
'title': title,
'artist': artist,
'author': author,
'description': description,
'genre': genre?.join(", "),
'status': status,
'thumbnail_url': thumbnailUrl,
};
}
}

275
lib/eval/mihon/service.dart Normal file
View file

@ -0,0 +1,275 @@
import 'dart:convert';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:mangayomi/eval/model/filter.dart';
import 'package:mangayomi/eval/model/m_chapter.dart';
import 'package:mangayomi/eval/model/m_manga.dart';
import 'package:mangayomi/eval/model/m_pages.dart';
import 'package:mangayomi/eval/model/source_preference.dart';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/video.dart';
import 'package:mangayomi/services/http/m_client.dart';
import '../../models/manga.dart';
import '../interface.dart';
import 'models.dart';
class MihonExtensionService implements ExtensionService {
late String androidProxyServer;
@override
late Source source;
late InterceptedClient client;
MihonExtensionService(this.source, this.androidProxyServer) {
client = MClient.init();
}
@override
Map<String, String> getHeaders() {
return {};
}
@override
bool get supportsLatest {
return true;
}
@override
String get sourceBaseUrl {
return source.baseUrl!;
}
@override
Future<MPages> getPopular(int page) async {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getPopular$name",
"page": page + 1,
"search": "",
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
final pages = MangaPages.fromJson(data, source.itemType);
return MPages(
list: pages.list
.map(
(e) => MManga(
name: e.title,
link: e.url,
artist: e.artist,
author: e.author,
description: e.description,
genre: e.genre,
status: e.status,
imageUrl: e.thumbnailUrl,
chapters: [],
),
)
.toList(),
hasNextPage: pages.hasNextPage,
);
}
@override
Future<MPages> getLatestUpdates(int page) async {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getLatest$name",
"page": page + 1,
"search": "",
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
final pages = MangaPages.fromJson(data, source.itemType);
return MPages(
list: pages.list
.map(
(e) => MManga(
name: e.title,
link: e.url,
artist: e.artist,
author: e.author,
description: e.description,
genre: e.genre,
status: e.status,
imageUrl: e.thumbnailUrl,
chapters: [],
),
)
.toList(),
hasNextPage: pages.hasNextPage,
);
}
@override
Future<MPages> search(String query, int page, List<dynamic> filters) async {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getSearch$name",
"page": page + 1,
"search": query,
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
final pages = MangaPages.fromJson(data, source.itemType);
return MPages(
list: pages.list
.map(
(e) => MManga(
name: e.title,
link: e.url,
artist: e.artist,
author: e.author,
description: e.description,
genre: e.genre,
status: e.status,
imageUrl: e.thumbnailUrl,
chapters: [],
),
)
.toList(),
hasNextPage: pages.hasNextPage,
);
}
@override
Future<MManga> getDetail(String url) async {
final name = source.itemType == ItemType.anime ? "Anime" : "Manga";
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getDetails$name",
if (source.itemType == ItemType.manga) "mangaData": {"url": url},
if (source.itemType == ItemType.anime) "animeData": {"url": url},
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
final chapters = await getChapterList(url);
return MManga(
name: data['title'],
link: data['url'],
artist: data['artist'],
author: data['author'],
description: data['description'],
genre: (data['genres'] as List?)?.map((e) => e.toString()).toList() ?? [],
status: switch (data['status'] as int?) {
1 => Status.ongoing,
2 => Status.completed,
4 => Status.publishingFinished,
5 => Status.canceled,
6 => Status.onHiatus,
_ => Status.unknown,
},
imageUrl: data['thumbnail_url'],
chapters: chapters,
);
}
Future<List<MChapter>> getChapterList(String url) async {
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": source.itemType == ItemType.anime
? "getEpisodeList"
: "getChapterList",
if (source.itemType == ItemType.manga) "mangaData": {"url": url},
if (source.itemType == ItemType.anime) "animeData": {"url": url},
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as List;
return data
.map(
(e) => MChapter(
name: e['name'],
url: e['url'],
dateUpload: e['date_upload'] is int
? (e['date_upload'] as int).toString()
: e['date_upload'],
scanlator: e['scanlator'],
),
)
.toList();
}
@override
Future<List<PageUrl>> getPageList(String url) async {
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getPageList",
"chapterData": {"url": url},
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as List;
return data.map((e) => PageUrl(e['url'])).toList();
}
@override
Future<List<Video>> getVideoList(String url) async {
final res = await client.post(
Uri.parse("$androidProxyServer/dalvik"),
body: jsonEncode({
"method": "getVideoList",
"episodeData": {"url": url},
"data": source.sourceCode,
}),
);
final data = jsonDecode(res.body) as List;
return data.map((e) {
final tempHeaders =
e['headers']['namesAndValues\$okhttp'] as List<dynamic>;
final Map<String, String> headers = {};
for (var i = 0; i + 1 < tempHeaders.length; i++) {
headers[tempHeaders[i]] = tempHeaders[i + 1];
}
return Video(
e['videoUrl'],
e['quality'],
e['url'],
headers: headers,
audios:
(e['audioTracks'] as List?)
?.map((e) => Track(file: e['file'], label: e['label']))
.toList() ??
[],
subtitles:
(e['subtitleTracks'] as List?)
?.map((e) => Track(file: e['file'], label: e['label']))
.toList() ??
[],
);
}).toList();
}
@override
Future<String> getHtmlContent(String name, String url) async {
return "";
}
@override
Future<String> cleanHtmlContent(String html) async {
return html;
}
@override
FilterList getFilterList() {
return FilterList([]);
}
@override
List<SourcePreference> getSourcePreferences() {
return [];
}
}

View file

@ -201,6 +201,8 @@ class Settings {
List<Repo>? novelExtensionsRepo;
String? androidProxyServer;
@enumerated
late SectionType disableSectionType;
@ -371,6 +373,7 @@ class Settings {
this.mangaExtensionsRepo,
this.animeExtensionsRepo,
this.novelExtensionsRepo,
this.androidProxyServer,
this.lastTrackerLibraryLocation,
this.mergeLibraryNavMobile = false,
this.enableDiscordRpc = true,
@ -594,6 +597,7 @@ class Settings {
.map((e) => Repo.fromJson(e))
.toList();
}
androidProxyServer = json['androidProxyServer'];
lastTrackerLibraryLocation = json['lastTrackerLibraryLocation'];
mergeLibraryNavMobile = json['mergeLibraryNavMobile'];
enableDiscordRpc = json['enableDiscordRpc'];
@ -736,6 +740,7 @@ class Settings {
'mangaExtensionsRepo': mangaExtensionsRepo?.map((e) => e.toJson()).toList(),
'animeExtensionsRepo': animeExtensionsRepo?.map((e) => e.toJson()).toList(),
'novelExtensionsRepo': novelExtensionsRepo?.map((e) => e.toJson()).toList(),
'androidProxyServer': androidProxyServer,
'lastTrackerLibraryLocation': lastTrackerLibraryLocation,
'mergeLibraryNavMobile': mergeLibraryNavMobile,
'enableDiscordRpc': enableDiscordRpc,

File diff suppressed because it is too large Load diff

View file

@ -191,4 +191,4 @@ class Source {
}
}
enum SourceCodeLanguage { dart, javascript }
enum SourceCodeLanguage { dart, javascript, mihon }

View file

@ -489,10 +489,12 @@ const _SourceitemTypeValueEnumMap = {
const _SourcesourceCodeLanguageEnumValueMap = {
'dart': 0,
'javascript': 1,
'mihon': 2,
};
const _SourcesourceCodeLanguageValueEnumMap = {
0: SourceCodeLanguage.dart,
1: SourceCodeLanguage.javascript,
2: SourceCodeLanguage.mihon,
};
Id _sourceGetId(Source object) {

View file

@ -6,7 +6,7 @@ part of 'aniskip.dart';
// RiverpodGenerator
// **************************************************************************
String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421';
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
/// See also [AniSkip].
@ProviderFor(AniSkip)

View file

@ -25,7 +25,56 @@ Future<void> fetchSourcesList({
final info = await PackageInfo.fromPlatform();
final sourceList = (jsonDecode(req.body) as List)
.map((e) => Source.fromJson(e))
.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!";
src.id = 'mihon-${source['id']}'.hashCode;
yield src;
}
} else {
yield Source.fromJson(e);
}
})
.where(
(source) =>
source.itemType == itemType &&
@ -74,14 +123,17 @@ Future<void> _updateSource(
) 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 headers = getExtensionService(
source..sourceCode = req.body,
source..sourceCode = sourceCode,
).getHeaders();
final updatedSource = Source()
..headers = jsonEncode(headers)
..isAdded = true
..sourceCode = req.body
..sourceCode = sourceCode
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
..apiUrl = source.apiUrl

View file

@ -6,7 +6,7 @@ part of 'sync_server.dart';
// RiverpodGenerator
// **************************************************************************
String _$syncServerHash() => r'141ba3be28182e05480e06fbf3f1de68f868cb8e';
String _$syncServerHash() => r'08225f80e9c249dc62e8e918acecbd593f54541f';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'anilist.dart';
// RiverpodGenerator
// **************************************************************************
String _$anilistHash() => r'c786a526fdacc875e4a7e00886b2bda546eafeae';
String _$anilistHash() => r'fafb964252b3a5741e981cb8c2f0f2090b3b86ae';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'kitsu.dart';
// RiverpodGenerator
// **************************************************************************
String _$kitsuHash() => r'e24e9b57cfea974110d1f7c704c306c3b58e3529';
String _$kitsuHash() => r'd46b955c92bc4d7382d32e17827da2e2b3a8434f';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'myanimelist.dart';
// RiverpodGenerator
// **************************************************************************
String _$myAnimeListHash() => r'4391ad9446d14b1fb1ffdfbc5323ef04db5140f7';
String _$myAnimeListHash() => r'739c836ddbfc7c2c2b7593304f481e8d35074391';
/// Copied from Dart SDK
class _SystemHash {