Merge branch 'main' into feature/light-novel

This commit is contained in:
Moustapha Kodjo Amadou
2025-01-06 15:41:28 +01:00
committed by GitHub
6 changed files with 1889 additions and 62 deletions

View File

@@ -2,7 +2,7 @@ import '../../../../model/source.dart';
import 'src/hianime/hianime.dart'; import 'src/hianime/hianime.dart';
import 'src/kaido/kaido.dart'; import 'src/kaido/kaido.dart';
const _zorothemeVersion = "0.0.95"; const _zorothemeVersion = "0.1.0";
const _zorothemeSourceCodeUrl = const _zorothemeSourceCodeUrl =
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/zorotheme/zorotheme.dart"; "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/anime/multisrc/zorotheme/zorotheme.dart";
@@ -15,5 +15,6 @@ List<Source> _zorothemeSourcesList = [
] ]
.map((e) => e .map((e) => e
..sourceCodeUrl = _zorothemeSourceCodeUrl ..sourceCodeUrl = _zorothemeSourceCodeUrl
..appMinVerReq = "0.4.0"
..version = _zorothemeVersion) ..version = _zorothemeVersion)
.toList(); .toList();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
const mangayomiSources = [{ const mangayomiSources = [{
"name": "Autoembed", "name": "Autoembed",
"lang": "all", "lang": "all",
"baseUrl": "https://autoembed.cc", "baseUrl": "https://watch.autoembed.cc",
"apiUrl": "https://tom.autoembed.cc", "apiUrl": "https://tom.autoembed.cc",
"iconUrl": "https://www.google.com/s2/favicons?sz=64&domain=https://autoembed.cc/", "iconUrl": "https://www.google.com/s2/favicons?sz=64&domain=https://autoembed.cc/",
"typeSource": "multi", "typeSource": "multi",
"isManga": false, "isManga": false,
"version": "1.0.1", "version": "1.1.4",
"dateFormat": "", "dateFormat": "",
"dateFormatLocale": "", "dateFormatLocale": "",
"pkgPath": "anime/src/all/autoembed.js" "pkgPath": "anime/src/all/autoembed.js"
@@ -14,7 +14,7 @@ const mangayomiSources = [{
class DefaultExtension extends MProvider { class DefaultExtension extends MProvider {
getHeaders(url) { getHeaders() {
return { return {
Referer: this.source.apiUrl Referer: this.source.apiUrl
} }
@@ -59,9 +59,17 @@ class DefaultExtension extends MProvider {
body = await this.tmdbRequest(`catalog/series/${slug}`); body = await this.tmdbRequest(`catalog/series/${slug}`);
var popSeries = await this.getSearchItems(body); var popSeries = await this.getSearchItems(body);
var fullList = [];
var priority = await this.getPreference("pref_content_priority");
if (priority === "series") {
fullList = [...popSeries, ...popMovie];
} else {
fullList = [...popMovie, ...popSeries]
}
var hasNextPage = slug.indexOf("search=") > -1 ? false : true; var hasNextPage = slug.indexOf("search=") > -1 ? false : true;
return { return {
list: [...popMovie, ...popSeries], list: fullList,
hasNextPage hasNextPage
}; };
@@ -90,8 +98,9 @@ class DefaultExtension extends MProvider {
var body = await this.tmdbRequest(`meta/${media_type}/${id}.json`) var body = await this.tmdbRequest(`meta/${media_type}/${id}.json`)
var result = body.meta; var result = body.meta;
var tmdb_id = id.substring(5, ) var tmdb_id = id.substring(5,)
var imdb_id = result.imdb_id media_type = media_type == "series" ? "tv" : media_type;
var dateNow = Date.now().valueOf(); var dateNow = Date.now().valueOf();
var release = result.released ? new Date(result.released).valueOf() : dateNow var release = result.released ? new Date(result.released).valueOf() : dateNow
var chaps = []; var chaps = [];
@@ -99,12 +108,14 @@ class DefaultExtension extends MProvider {
var item = { var item = {
name: result.name, name: result.name,
imageUrl: result.poster, imageUrl: result.poster,
link: `https://imdb.com/title/${imdb_id}`, link: `${this.source.baseUrl}/${media_type}/${tmdb_id}`,
description: result.description, description: result.description,
genre: result.genre, genre: result.genre,
}; };
if (media_type == "series") { var link = `${media_type}||${tmdb_id}`
if (media_type == "tv") {
var videos = result.videos var videos = result.videos
for (var i in videos) { for (var i in videos) {
@@ -118,7 +129,7 @@ class DefaultExtension extends MProvider {
if (release < dateNow) { if (release < dateNow) {
var episodeNum = video.episode var episodeNum = video.episode
var name = `S${seasonNum}:E${episodeNum} - ${video.name}` var name = `S${seasonNum}:E${episodeNum} - ${video.name}`
var eplink = `tv||${tmdb_id}/${seasonNum}/${episodeNum}` var eplink = `${link}||${seasonNum}||${episodeNum}`
chaps.push({ chaps.push({
name: name, name: name,
@@ -131,7 +142,7 @@ class DefaultExtension extends MProvider {
if (release < dateNow) { if (release < dateNow) {
chaps.push({ chaps.push({
name: "Movie", name: "Movie",
url: `${media_type}||${tmdb_id}`, url: link,
dateUpload: release.toString(), dateUpload: release.toString(),
}) })
} }
@@ -141,27 +152,57 @@ class DefaultExtension extends MProvider {
chaps.reverse(); chaps.reverse();
return item; return item;
} }
async extractStreams(url) {
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url, lang = "", hdr = {}) {
const response = await new Client().get(url); const response = await new Client().get(url);
const body = response.body; const body = response.body;
const lines = body.split('\n'); const lines = body.split('\n');
var streams = []; var streams = [{
url: url,
originalUrl: url,
quality: "auto",
}];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) { if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
const resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1]; var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
const m3u8Url = lines[i + 1].trim(); resolution = `${lang} ${resolution}`
var m3u8Url = lines[i + 1].trim();
m3u8Url = m3u8Url.replace("./", `${url}/`)
streams.push({ streams.push({
url: m3u8Url, url: m3u8Url,
originalUrl: m3u8Url, originalUrl: m3u8Url,
quality: resolution, quality: resolution,
headers: hdr
}); });
} }
} }
return streams
}
// For some streams, we can form stream url using a default template.
async splitStreams(url, lang = "", hdr = {}) {
var streams = [];
var quality = ["auto", "360", "480", "720", "1080"]
for (var q of quality) {
var link = url
if (q != "auto") {
link = link.replace("index.m3u8", `${q}/index.m3u8`)
q = `${q}p`
}
streams.push({
url: link,
originalUrl: link,
quality: `${lang} - ${q}`,
headers: hdr
});
}
return streams; return streams;
} }
// Sorts streams based on user preference.
async sortStreams(streams) { async sortStreams(streams) {
var sortedStreams = []; var sortedStreams = [];
@@ -181,31 +222,177 @@ class DefaultExtension extends MProvider {
return [...sortedStreams, ...copyStreams] return [...sortedStreams, ...copyStreams]
} }
// Gets subtitles based on TMDB id.
async getSubtitleList(id, s, e) {
var api = `https://sub.wyzie.ru/search?id=${id}`
if (s != "0") api = `${api}&season=${s}&episode=${e}`
var response = await new Client().get(api);
var body = JSON.parse(response.body);
var subs = []
for (var sub of body) {
subs.push({
file: sub.url,
label: sub.display
})
}
return subs
}
// For anime episode video list // For anime episode video list
async getVideoList(url) { async getVideoList(url) {
var streamAPI = parseInt(await this.getPreference("pref_stream_source"))
var parts = url.split("||"); var parts = url.split("||");
var media_type = parts[0]; var media_type = parts[0];
var id = parts[1]; var id = parts[1];
var api = `${this.source.apiUrl}/api/getVideoSource?type=${media_type}&id=${id}` var tmdb = id
const response = await new Client().get(api, this.getHeaders()); var streams = []
const body = JSON.parse(response.body); var subtitles = []
switch (streamAPI) {
case 2: {
var s = "0"
var e = "0"
if (media_type == "tv") {
s = parts[2]
e = parts[3]
id = `${id}/${s}/${e}`
}
var api = `https://play2.123embed.net/server/3?path=/${media_type}/${id}`
var response = await new Client().get(api);
if (response.statusCode == 404) { if (response.statusCode != 200) {
throw new Error("Video unavailable"); throw new Error("Video unavailable");
}
var body = JSON.parse(response.body);
var link = body.playlist[0].file
streams.push({
url: link,
originalUrl: link,
quality: "auto",
headers: { "Origin": "https://play2.123embed.net" },
});
break;
}
case 3: {
var s = "0"
var e = "0"
if (media_type == "tv") {
s = parts[2]
e = parts[3]
id = `${id}&s=${s}&e=${e}`
}
var api = `https://autoembed.cc/embed/player.php?id=${id}`
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error("Video unavailable");
}
var body = response.body
var sKey = '"file": '
var eKey = "]});"
var start = body.indexOf(sKey)
if (start < 0) {
throw new Error("Video unavailable");
}
start += sKey.length
var end = body.substring(start,).indexOf(eKey) + start - 1
var strms = JSON.parse(body.substring(start, end) + "]")
for (var strm of strms) {
var link = strm.file
var lang = strm.title
var streamSplit = await this.splitStreams(link, lang);
streams = [...streams, ...streamSplit]
}
break;
}
case 4: {
var s = "0"
var e = "0"
if (media_type == "tv") {
s = parts[2]
e = parts[3]
id = `${id}&season=${s}&episode=${e}`
}
var api = `https://player.flicky.host/Server-main.php?id=${id}`
var response = await new Client().get(api, { "Referer": "https://flicky.host/" });
if (response.statusCode != 200) {
throw new Error("Video unavailable");
}
var body = response.body
var sKey = 'streams = '
var eKey = "];"
var start = body.indexOf(sKey)
if (start < 0) {
throw new Error("Video unavailable");
}
start += sKey.length
var end = body.substring(start,).indexOf(eKey) + start + 1
var strms = JSON.parse(body.substring(start, end))
for (var strm of strms) {
var link = strm.url
var lang = strm.language
var streamSplit = await this.splitStreams(link, lang);
streams = [...streams, ...streamSplit]
}
break;
}
case 5: {
if (media_type == "tv") {
id = `${id}/${parts[2]}/${parts[3]}`
}
var api = `https://vidapi.click/api/video/${media_type}/${id}`
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error("Video unavailable");
}
var body = JSON.parse(response.body);
var link = body.sources[0].file
subtitles = body.tracks
streams = await this.extractStreams(link);
break;
}
default: {
if (media_type == "tv") {
id = `${id}/${parts[2]}/${parts[3]}`
}
var api = `${this.source.apiUrl}/api/getVideoSource?type=${media_type}&id=${id}`
var response = await new Client().get(api, this.getHeaders());
if (response.statusCode != 200) {
throw new Error("Video unavailable");
}
var body = JSON.parse(response.body);
var link = body.videoSource
subtitles = body.subtitles
streams = await this.extractStreams(link);
break;
}
} }
var link = body.videoSource if (streams.length < 1) {
throw new Error("Video unavailable");
}
var subtitles = body.subtitles if (subtitles.length < 1) {
var streams = await this.extractStreams(link); subtitles = await this.getSubtitleList(tmdb, s, e)
streams.push({ }
url: link, streams[0].subtitles = subtitles
originalUrl: link,
quality: "auto",
subtitles: subtitles,
});
return await this.sortStreams(streams); return await this.sortStreams(streams)
} }
// For manga chapter pages // For manga chapter pages
async getPageList() { async getPageList() {
@@ -217,27 +404,44 @@ class DefaultExtension extends MProvider {
getSourcePreferences() { getSourcePreferences() {
return [{ return [{
key: 'pref_latest_time_window', key: 'pref_latest_time_window',
listPreference: { listPreference: {
title: 'Preferred latest trend time window', title: 'Preferred latest trend time window',
summary: '', summary: '',
valueIndex: 0, valueIndex: 0,
entries: ["Day", "Week"], entries: ["Day", "Week"],
entryValues: ["day", "week"] entryValues: ["day", "week"]
} }
}, { }, {
key: 'pref_video_resolution', key: 'pref_video_resolution',
listPreference: { listPreference: {
title: 'Preferred video resolution', title: 'Preferred video resolution',
summary: '', summary: '',
valueIndex: 0, valueIndex: 0,
entries: ["Auto", "1080p", "720p", "360p"], entries: ["Auto", "1080p", "720p", "360p"],
entryValues: ["auto", "1080", "720", "360"] entryValues: ["auto", "1080", "720", "360"]
} }
}, }, {
key: 'pref_content_priority',
listPreference: {
title: 'Preferred content priority',
summary: 'Choose which type of content to show first',
valueIndex: 0,
entries: ["Movies", "Series"],
entryValues: ["movies", "series"]
}
},
{
key: 'pref_stream_source',
listPreference: {
title: 'Preferred stream source',
summary: '',
valueIndex: 0,
entries: ["tom.autoembed.cc", "123embed.net", "autoembed.cc - Indian languages", "flicky.host - Indian languages", "vidapi.click"],
entryValues: ["1", "2", "3", "4", "5"]
}
},
]; ];
} }
} }

View File

@@ -5,8 +5,8 @@ const mangayomiSources = [{
"apiUrl": "", "apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png", "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png",
"typeSource": "single", "typeSource": "single",
"itemType": 1, "isManga": false,
"version": "0.0.55", "version": "0.0.6",
"dateFormat": "", "dateFormat": "",
"dateFormatLocale": "", "dateFormatLocale": "",
"pkgPath": "anime/src/all/netflixmirror.js" "pkgPath": "anime/src/all/netflixmirror.js"
@@ -22,8 +22,10 @@ class DefaultExtension extends MProvider {
if (elements && elements.length > 0) { if (elements && elements.length > 0) {
return cookie; return cookie;
} }
const addhash = new Document((await new Client().get(`${this.source.baseUrl}/home`, { "cookie": "" })).body).selectFirst("body").attr("data-addhash"); const hDoc = new Document((await new Client().get(`${this.source.baseUrl}/home`, { "cookie": "" })).body);
await new Client().get(`https://userverify.netmirror.app/verify?vhfd=${addhash}&a=y`); const addhash = hDoc.selectFirst("body").attr("data-addhash");
const time = hDoc.selectFirst("body").attr("data-time");
await new Client().get(`https://userverify.netmirror.app/?fr3=${addhash}&a=y&t=${time}`);
let body; let body;
let res; let res;
do { do {
@@ -209,4 +211,4 @@ class DefaultExtension extends MProvider {
return videoList; return videoList;
} }
} }

View File

@@ -0,0 +1,280 @@
const mangayomiSources = [{
"name": "Mangapill",
"lang": "en",
"baseUrl": "https://mangapill.com",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=64&domain=https://mangapill.com/",
"typeSource": "single",
"isManga": true,
"version": "1.0.2",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "manga/src/en/mangapill.js"
}];
class DefaultExtension extends MProvider {
getHeaders(url) {
return {
"Referer": this.source.baseUrl
}
}
statusCode(status) {
return {
"publishing": 0,
"finished": 1,
"on hiatus": 2,
"discontinued": 3,
"not yet published": 4,
}[status] ?? 5;
}
async getPreference(key) {
const preferences = new SharedPreferences();
return parseInt(preferences.get(key))
}
async getMangaList(slug) {
var lang = await this.getPreference("pref_title_lang");
var url = `${this.source.baseUrl}/${slug}`
var res = await new Client().get(url, this.getHeaders());
var doc = new Document(res.body);
var list = [];
var mangaElements = doc.select("div.grid.gap-3.lg > div")
for (var manga of mangaElements) {
var details = manga.selectFirst('div').select('a');
var detLen = details.length
details = details[detLen - 1]
var imageUrl = manga.selectFirst("img").getSrc;
var link = details.getHref;
var nameSection = details.select('div');
var name = (nameSection[1] && lang == 2) ? nameSection[1].text : nameSection[0].text
list.push({ name, imageUrl, link });
}
var hasNextPage = false;
if (slug.includes("search?q")) {
hasNextPage = doc.selectFirst(".container.py-3 a.btn.btn-sm").className ? true : false
}
return { list, hasNextPage }
}
async getNavPage(prefKey) {
var val = await this.getPreference(prefKey);
var slug = ''
switch (val) {
case 1: {
slug = 'mangas/new'
break;
}
case 2: {
slug = 'chapters'
break;
}
}
return await this.getMangaList(slug)
}
async getPopular(page) {
return await this.getNavPage("pref_popular_content");
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
return await this.getNavPage("pref_latest_content");
}
async searchManga(query, status, type, genre, page) {
var slug = `search?q=${query}&status=${status}&type=${type}${genre}&page=${page}`
return await this.getMangaList(slug)
}
async search(query, page, filters) {
var type = filters[0].values[filters[0].state].value
var status = filters[1].values[filters[1].state].value
var genre = ""
for (var filter of filters[2].state) {
if (filter.state == true)
genre += `&genre=${filter.value}`
}
return await this.searchManga(query, status, type, genre, page);
}
async getMangaDetail(slug) {
var lang = await this.getPreference("pref_title_lang");
var link = `${this.source.baseUrl}${slug}`
var res = await new Client().get(link, this.getHeaders());
var doc = new Document(res.body);
var mangaName = doc.selectFirst(".mb-3 .font-bold.text-lg").text
if (doc.selectFirst(".mb-3 .text-sm.text-secondary") && lang == 2) mangaName = doc.selectFirst(".mb-3 .text-sm.text-secondary").text
var description = doc.selectFirst("meta[name='description']").attr("content")
var imageUrl = doc.selectFirst(".w-full.h-full").getSrc
var statusText = doc.select(".grid.grid-cols-1 > div")[1].selectFirst("div").text
var status = this.statusCode(statusText)
var genre = []
var genreList = doc.select("a.mr-1")
for (var gen of genreList) { genre.push(gen.text) }
var chapters = []
var chapList = doc.select("div.my-3.grid > a")
for (var chap of chapList) {
var name = chap.text
var url = chap.getHref
chapters.push({ name, url })
}
return { name: mangaName, description, link, imageUrl, status, genre, chapters }
}
async getDetail(url) {
return await this.getMangaDetail(url);
}
// For anime episode video list
async getVideoList(url) {
throw new Error("getVideoList not implemented");
}
// For manga chapter pages
async getPageList(url) {
var link = `${this.source.baseUrl}${url}`
var res = await new Client().get(link, this.getHeaders());
var doc = new Document(res.body);
var urls = [];
var pages = doc.select("chapter-page")
for (var page of pages) {
var img = page.selectFirst("img").getSrc
if (img != null) urls.push(img);
}
return urls
}
getFilterList() {
return [
{
type_name: "SelectFilter",
name: "Type",
state: 0,
values: [
["All", ""],
["Manga", "manga"],
["Novel", "novel"],
["One-Shot", "one-shot"],
["Doujinshi", "doujinshi"],
["Manhwa", "manhwa"],
["Manhua", "manhua"],
["Oel", "oel"]
].map(x => ({ type_name: 'SelectOption', name: x[0], value: x[1] }))
},
{
type_name: "SelectFilter",
name: "Status",
state: 0,
values: [
["All", ""],
["Publishing", "publishing"],
["Finished", "finished"],
["On hiatus", "on hiatus"],
["Discontinued", "discontinued"],
["Not yet published", "not yet published"]
].map(x => ({ type_name: 'SelectOption', name: x[0], value: x[1] }))
}, {
type_name: "GroupFilter",
name: "Genre",
state: [
["Action", "Action"],
["Adventure", "Adventure"],
["Cars", "Cars"],
["Comedy", "Comedy"],
["Dementia", "Dementia"],
["Demons", "Demons"],
["Doujinshi", "Doujinshi"],
["Drama", "Drama"],
["Ecchi", "Ecchi"],
["Fantasy", "Fantasy"],
["Game", "Game"],
["Gender Bender", "Gender Bender"],
["Harem", "Harem"],
["Historical", "Historical"],
["Horror", "Horror"],
["Isekai", "Isekai"],
["Josei", "Josei"],
["Kids", "Kids"],
["Magic", "Magic"],
["Martial Arts", "Martial Arts"],
["Mecha", "Mecha"],
["Military", "Military"],
["Music", "Music"],
["Mystery", "Mystery"],
["Parody", "Parody"],
["Police", "Police"],
["Psychological", "Psychological"],
["Romance", "Romance"],
["Samurai", "Samurai"],
["School", "School"],
["Sci-Fi", "Sci-Fi"],
["Seinen", "Seinen"],
["Shoujo", "Shoujo"],
["Shoujo Ai", "Shoujo Ai"],
["Shounen", "Shounen"],
["Shounen Ai", "Shounen Ai"],
["Slice of Life", "Slice of Life"],
["Space", "Space"],
["Sports", "Sports"],
["Super Power", "Super Power"],
["Supernatural", "Supernatural"],
["Thriller", "Thriller"],
["Tragedy", "Tragedy"],
["Vampire", "Vampire"],
["Yaoi", "Yaoi"],
["Yuri", "Yuri"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
}
];
}
getSourcePreferences() {
return [{
key: 'pref_popular_content',
listPreference: {
title: 'Preferred popular content',
summary: '',
valueIndex: 0,
entries: ["New Mangas", "Recent Chapters"],
entryValues: ["1", "2"]
}
}, {
key: 'pref_latest_content',
listPreference: {
title: 'Preferred latest content',
summary: '',
valueIndex: 1,
entries: ["New Mangas", "Recent Chapters"],
entryValues: ["1", "2"]
}
}, {
key: 'pref_title_lang',
listPreference: {
title: 'Preferred title language',
summary: '',
valueIndex: 0,
entries: ["Romaji", "English"],
entryValues: ["1", "2"]
}
}
];
}
}