Add support for My Manga Reader CMS sources (many, many sources) (#103)
* Add My Manga Reader CMS sources and generator Currently supported sources: - EN: Read Comics Online - EN: Fallen Angels Scans - EN: MangaRoot - EN: Mangawww Reader - EN: MangaForLife - ES: My-mangas.com - FA: TrinityReader - ID: Manga Desu - JA: IchigoBook - TR: MangAoi * Add more sources Code cleanup Added thumbnail guesser to keyword search Fix build Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: MangaRoot - EN: Mangawww Reader - EN: MangaForLife - EN: Manga Mofo - EN: H-Manga.moe - EN: MangaBlue - EN: Manga Forest - EN: DManga - ES: My-mangas.com - FA: TrinityReader - FR: Manga-LEL - FR: Manga Etonnia - FR: Tous Vos Scans - ID: Manga Desu - ID: Komik Mangafire.ID - ID: MangaOnline - ID: MangaNesia - ID: KOMIK.CO.ID - ID: MangaID - ID: Indo Manga Reader - JA: IchigoBook - JA: Mangaraw Online - PL: Candy Scans - PT: Comic Space - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta * Disable latest updates for sources do not support it * Latest updates support scanner no longer generates false positives * Fix source generator being included in APK Remove sources that went offline Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: MangaRoot - EN: Mangawww Reader - EN: MangaForLife - EN: Manga Mofo - EN: H-Manga.moe - EN: MangaBlue - EN: Manga Forest - EN: DManga - ES: My-mangas.com - FA: TrinityReader - FR: Manga-LEL - FR: Manga Etonnia - FR: Tous Vos Scans - ID: Manga Desu - ID: MangaOnline - ID: KOMIK.CO.ID - ID: MangaID - JA: Mangaraw Online - PL: Candy Scans - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta * Code cleanup Remove dead sources Fix announcements being recognized as chapters in some sources Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: Mangawww Reader - EN: MangaForLife - EN: Manga Mofo - EN: H-Manga.moe - EN: MangaBlue - EN: Manga Forest - EN: DManga - ES: My-mangas.com - FA: TrinityReader - FR: Manga-LEL - FR: Manga Etonnia - FR: Tous Vos Scans - ID: Manga Desu - ID: MangaOnline - ID: KOMIK.CO.ID - ID: MangaID - JA: Mangaraw Online - PL: Candy Scans - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta * Remove logging from source (as logging library is not available) * Fix HTML entities not being escaped Add some new sources and remove obsolete sources Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: Mangawww Reader - EN: MangaForLife - EN: Manga Spoil - EN: H-Manga.moe - EN: DManga - EN: Chibi Manga Reader - EN: ZXComic - ES: My-mangas.com - FA: TrinityReader - FR: Manga-LEL - FR: Manga Etonnia - ID: Manga Desu - ID: MangaOnline - ID: KOMIK.CO.ID - ID: MangaID - ID: Manga Seru - JA: Mangaraw Online - JA: Mangazuki RAWS - PL: Candy Scans - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta - OTHER: HentaiShark * Remove offline sources * Extend HttpSource instead of ParsedHttpSource * Update sources Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: MangaForLife - EN: Manga Spoil - EN: DManga - EN: Chibi Manga Reader - EN: ZXComic - EN: DB Manga - EN: Mangacox - EN: GO Manhwa - EN: Hentai2Manga - ES: My-mangas.com - ES: SOS Scanlation - FA: TrinityReader - FR: Manga-LEL - FR: Scan FR - ID: Manga Desu - ID: Komikid - ID: MangaID - ID: Manga Seru - JA: Mangaraw Online - JA: Mangazuki RAWS - JA: MangaRAW - PL: Candy Scans - PT: Mangás Yuri - RU: NAKAMA - RU: AkaiYuhiMun team - TR: MangAoi - TR: MangaHanta - TR: ManhuaTR - OTHER: HentaiShark * Change extension name and remove dead sources Currently supported sources: - AR: مانجا اون لاين - EN: Read Comics Online - EN: Fallen Angels Scans - EN: MangaForLife - EN: Manga Spoil - EN: DManga - EN: Chibi Manga Reader - EN: ZXComic - EN: Mangacox - EN: Hentai2Manga - ES: My-mangas.com - ES: SOS Scanlation - FA: TrinityReader - FR: Manga-LEL - FR: Scan FR - ID: Manga Desu - ID: Komikid - ID: MangaID - ID: Manga Seru - JA: Mangaraw Online - JA: Mangazuki RAWS - JA: MangaRAW - PL: Candy Scans - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta - TR: ManhuaTR - OTHER: HentaiShark * Add tag searching support Remove dead sources Enable dead sources that are now online Add some new sources Sources are now parsed from JSON (still hardcoded) Currently supported sources: - AR: مانجا اون لاين - AR: Manga FYI - EN: Read Comics Online - EN: Fallen Angels Scans - EN: Mangawww Reader - EN: MangaForLife - EN: Manga Spoil - EN: DManga - EN: Chibi Manga Reader - EN: ZXComic - EN: Mangacox - EN: KoManga - EN: Manganimecan - EN: Hentai2Manga - EN: White Cloud Pavilion - EN: 4 Manga - EN: XYXX.INFO - ES: My-mangas.com - ES: SOS Scanlation - FR: Manga-LEL - FR: Manga Etonnia - FR: Scan FR - FR: ScanFR.com - FR: Manga FYI - FR: Mugiwara - FR: scans-manga - ID: Manga Desu - ID: MangaOnline - ID: Komikid - ID: MangaID - ID: Manga Seru - ID: Manga FYI - JA: Mangazuki RAWS - JA: MangaRAW - PL: Candy Scans - PL: ToraScans - PT: Comic Space - PT: Mangás Yuri - RU: NAKAMA - TR: MangAoi - TR: MangaHanta - TR: ManhuaTR - VI: Fallen Angels Scans - OTHER: HentaiShark * Update source categories and tags * Add icon Remove dead source
This commit is contained in:
parent
ab6054944d
commit
318f335bf8
|
@ -0,0 +1,18 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
appName = 'Tachiyomi: My Manga Reader CMS (Many sources)'
|
||||||
|
pkgNameSuffix = 'all.mmrcms'
|
||||||
|
extClass = '.MyMangaReaderCMSSources'
|
||||||
|
extVersionCode = 1
|
||||||
|
extVersionSuffix = 1
|
||||||
|
libVersion = '1.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
provided "com.google.code.gson:gson:2.8.1"
|
||||||
|
provided "com.github.salomonbrys.kotson:kotson:2.5.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
|
@ -0,0 +1,346 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "My Manga Reader CMS source generator by: nulldev"
|
||||||
|
# CMS: https://getcyberworks.com/product/manga-reader-cms/
|
||||||
|
|
||||||
|
# Print a message out to stderr
|
||||||
|
function echoErr() {
|
||||||
|
echo "ERROR: $@" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Require that a command exists before continuing
|
||||||
|
function require() {
|
||||||
|
command -v $1 >/dev/null 2>&1 || { echoErr "This script requires $1 but it's not installed."; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define commands that this script depends on
|
||||||
|
require xmllint
|
||||||
|
require jq
|
||||||
|
require perl
|
||||||
|
require wget
|
||||||
|
require curl
|
||||||
|
require grep
|
||||||
|
require sed
|
||||||
|
|
||||||
|
# Show help/usage info
|
||||||
|
function printHelp() {
|
||||||
|
echo "Usage: ./genSources.sh [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo "--help: Show this help page"
|
||||||
|
echo "--dry-run: Perform a dry run (make no changes)"
|
||||||
|
echo "--list: List currently available sources"
|
||||||
|
echo "--out <file>: Explicitly specify output file"
|
||||||
|
}
|
||||||
|
# Target file
|
||||||
|
TARGET="src/eu/kanade/tachiyomi/extension/all/mmrcms/GeneratedSources.kt"
|
||||||
|
# String containing processed URLs (used to detect duplicate URLs)
|
||||||
|
PROCESSED=""
|
||||||
|
|
||||||
|
# Parse CLI args
|
||||||
|
while [ $# -gt 0 ]
|
||||||
|
do
|
||||||
|
case "$1" in
|
||||||
|
--help)
|
||||||
|
printHelp
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--dry-run) OPT_DRY_RUN=true
|
||||||
|
;;
|
||||||
|
--list)
|
||||||
|
OPT_DRY_RUN=true
|
||||||
|
OPT_LIST=true
|
||||||
|
;;
|
||||||
|
--out)
|
||||||
|
TARGET="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Invalid option $1!"
|
||||||
|
printHelp
|
||||||
|
exit -1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid argument $1!"
|
||||||
|
printHelp
|
||||||
|
exit -1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# Change target if performing dry run
|
||||||
|
if [ "$OPT_DRY_RUN" = true ] ; then
|
||||||
|
# Do not warn if dry running because of list
|
||||||
|
if ! [ "$OPT_LIST" = true ] ; then
|
||||||
|
echo "Performing a dry run, no changes will be made!"
|
||||||
|
fi
|
||||||
|
TARGET="/dev/null"
|
||||||
|
else
|
||||||
|
# Delete old sources
|
||||||
|
rm "$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Variable used to store output while processing
|
||||||
|
QUEUED_SOURCES="["
|
||||||
|
|
||||||
|
# lang, name, baseUrl
|
||||||
|
function gen() {
|
||||||
|
PROCESSED="$PROCESSED$3\n"
|
||||||
|
if [ "$OPT_LIST" = true ] ; then
|
||||||
|
echo "- $(echo "$1" | awk '{print toupper($0)}'): $2"
|
||||||
|
else
|
||||||
|
echo "Generating source: $2"
|
||||||
|
QUEUED_SOURCES="$QUEUED_SOURCES"$'\n'"$(genSource "$1" "$2" "$3")"
|
||||||
|
# genSource runs in a subprocess, so we check for bad exit code and exit current process if necessary
|
||||||
|
[ $? -ne 0 ] && exit -1;
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find and get the item URL from an HTML page
|
||||||
|
function getItemUrl() {
|
||||||
|
grep -oP "(?<=showURL = \")(.*)(?=SELECTION)" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strip all scripts and Cloudflare email protection from page
|
||||||
|
# We strip Cloudflare email protection as titles like 'IDOLM@STER' can trigger it and break the parser
|
||||||
|
function stripScripts() {
|
||||||
|
perl -0pe 's/<script.*?>[\s\S]*?< *?\/ *?script *?>//g' |\
|
||||||
|
perl -0pe 's/<span class="__cf_email__".*?>[\s\S]*?< *?\/ *?span *?>/???@???/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify that a response is valid
|
||||||
|
function verifyResponse() {
|
||||||
|
[ "${1##*$'\n'}" -eq "200" ] && [[ "$1" != *"Whoops, looks like something went wrong"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the available tags from the manga list page
|
||||||
|
function parseTagsFromMangaList() {
|
||||||
|
xmllint --xpath "//div[contains(@class, 'tag-links')]//a" --html "$1" 2>/dev/null |\
|
||||||
|
sed 's/<\/a>/"},\n/g; s/">/", "name": "/g;' |\
|
||||||
|
perl -pe 's/<a.*?\/tag\// {"id": "/gi;' |\
|
||||||
|
sed '/^</d'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the available categories from the manga list page
|
||||||
|
function parseCategoriesFromMangaList() {
|
||||||
|
xmllint --xpath "//li//a[contains(@class, 'category')]" --html "$1" 2>/dev/null |\
|
||||||
|
sed 's/<\/a>/"},\n/g; s/" class="category">/", "name": "/g;' |\
|
||||||
|
perl -pe 's/<a.*?\?cat=/ {"id": "/gi;'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the available categories from the advanced search page
|
||||||
|
function parseCategoriesFromAdvancedSearch() {
|
||||||
|
xmllint --xpath "//select[@name='categories[]']/option" --html "$1" 2>/dev/null |\
|
||||||
|
sed 's/<\/option>/"},\n/g; s/<option value="/ {"id": "/g; s/">/", "name": "/g;'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unescape HTML entities
|
||||||
|
function unescapeHtml() {
|
||||||
|
echo "$1" | perl -C -MHTML::Entities -pe 'decode_entities($_);'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the last character from a string, often used to remove the trailing comma
|
||||||
|
function stripLastComma() {
|
||||||
|
echo "${1::-1}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# lang, name, baseUrl
|
||||||
|
function genSource() {
|
||||||
|
# Allocate temp files
|
||||||
|
DL_TMP="$(mktemp)"
|
||||||
|
PG_TMP="$(mktemp)"
|
||||||
|
|
||||||
|
# Fetch categories from advanced search
|
||||||
|
wget "$3/advanced-search" -O "$DL_TMP"
|
||||||
|
# Find manga/comic URL
|
||||||
|
ITEM_URL="$(getItemUrl "$DL_TMP")"
|
||||||
|
# Remove scripts
|
||||||
|
cat "$DL_TMP" | stripScripts > "$PG_TMP"
|
||||||
|
# Find and transform categories
|
||||||
|
CATEGORIES="$(parseCategoriesFromAdvancedSearch "$PG_TMP")"
|
||||||
|
# Get item url from home page if not on advanced search page!
|
||||||
|
if [[ -z "${ITEM_URL// }" ]]; then
|
||||||
|
# Download home page
|
||||||
|
wget "$3" -O "$DL_TMP"
|
||||||
|
# Extract item url again
|
||||||
|
ITEM_URL="$(getItemUrl "$DL_TMP")"
|
||||||
|
# Still missing?
|
||||||
|
if [[ -z "${ITEM_URL// }" ]]; then
|
||||||
|
echoErr "Could not get item URL!"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculate location of manga list page
|
||||||
|
LIST_URL_PREFIX="manga"
|
||||||
|
# Get last path item in item URL and set as URL prefix
|
||||||
|
if [[ $ITEM_URL =~ .*\/([^\\]+)\/ ]]; then
|
||||||
|
LIST_URL_PREFIX="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
# Download manga list page
|
||||||
|
wget "$3/$LIST_URL_PREFIX-list" -O "$DL_TMP"
|
||||||
|
# Remove scripts
|
||||||
|
cat "$DL_TMP" | stripScripts > "$PG_TMP"
|
||||||
|
|
||||||
|
# Get categories from manga list page if we couldn't from advanced search
|
||||||
|
if [[ -z "${CATEGORIES// }" ]]; then
|
||||||
|
# Parse
|
||||||
|
CATEGORIES="$(parseCategoriesFromMangaList "$PG_TMP")"
|
||||||
|
# Check again
|
||||||
|
if [[ -z "${CATEGORIES// }" ]]; then
|
||||||
|
echoErr "Could not get categories!"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get tags from manga list page
|
||||||
|
TAGS="$(parseTagsFromMangaList "$PG_TMP")"
|
||||||
|
if [[ -z "${TAGS// }" ]]; then
|
||||||
|
TAGS="null"
|
||||||
|
else
|
||||||
|
TAGS="$(stripLastComma "$TAGS")"
|
||||||
|
TAGS=$'[\n'"$TAGS"$'\n ]'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unescape HTML entities
|
||||||
|
CATEGORIES="$(unescapeHtml "$CATEGORIES")"
|
||||||
|
# Check if latest manga is supported
|
||||||
|
LATEST_RESP=$(curl --write-out \\n%{http_code} --silent --output - "$3/filterList?page=1&sortBy=last_release&asc=false")
|
||||||
|
SUPPORTS_LATEST="false"
|
||||||
|
if verifyResponse "$LATEST_RESP"; then
|
||||||
|
SUPPORTS_LATEST="true"
|
||||||
|
fi
|
||||||
|
# Remove leftover html pages
|
||||||
|
rm "$DL_TMP"
|
||||||
|
rm "$PG_TMP"
|
||||||
|
|
||||||
|
# Cleanup categories
|
||||||
|
CATEGORIES="$(stripLastComma "$CATEGORIES")"
|
||||||
|
|
||||||
|
echo " {"
|
||||||
|
echo " \"language\": \"$1\","
|
||||||
|
echo " \"name\": \"$2\","
|
||||||
|
echo " \"base_url\": \"$3\","
|
||||||
|
echo " \"supports_latest\": $SUPPORTS_LATEST,"
|
||||||
|
echo " \"item_url\": \"$ITEM_URL\","
|
||||||
|
echo " \"categories\": ["
|
||||||
|
echo "$CATEGORIES"
|
||||||
|
echo " ],"
|
||||||
|
echo " \"tags\": $TAGS"
|
||||||
|
echo " },"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Source list
|
||||||
|
gen "ar" "مانجا اون لاين" "http://www.on-manga.com"
|
||||||
|
gen "ar" "Manga FYI" "http://mangafyi.com/manga/arabic"
|
||||||
|
gen "en" "Read Comics Online" "http://readcomics.website"
|
||||||
|
gen "en" "Fallen Angels Scans" "http://manga.fascans.com"
|
||||||
|
# Went offline
|
||||||
|
# gen "en" "MangaRoot" "http://mangaroot.com"
|
||||||
|
gen "en" "Mangawww Reader" "http://mangawww.com"
|
||||||
|
gen "en" "MangaForLife" "http://manga4ever.com"
|
||||||
|
gen "en" "Manga Spoil" "http://mangaspoil.com"
|
||||||
|
# Protected by CloudFlare
|
||||||
|
# gen "en" "MangaBlue" "http://mangablue.com"
|
||||||
|
# Some sort of anti-bot system
|
||||||
|
# gen "en" "Manga Forest" "https://mangaforest.com"
|
||||||
|
gen "en" "DManga" "http://dmanga.website"
|
||||||
|
gen "en" "Chibi Manga Reader" "http://www.cmreader.info"
|
||||||
|
gen "en" "ZXComic" "http://zxcomic.com"
|
||||||
|
# Went offline
|
||||||
|
# gen "en" "DB Manga" "http://dbmanga.com"
|
||||||
|
gen "en" "Mangacox" "http://mangacox.com"
|
||||||
|
# Protected by CloudFlare
|
||||||
|
# gen "en" "GO Manhwa" "http://gomanhwa.xyz"
|
||||||
|
# Went offline
|
||||||
|
# gen "en" "KoManga" "https://komanga.net"
|
||||||
|
gen "en" "Manganimecan" "http://manganimecan.com"
|
||||||
|
gen "en" "Hentai2Manga" "http://hentai2manga.com"
|
||||||
|
gen "en" "White Cloud Pavilion" "http://www.whitecloudpavilion.com/manga/free"
|
||||||
|
gen "en" "4 Manga" "http://4-manga.com"
|
||||||
|
gen "en" "XYXX.INFO" "http://xyxx.info"
|
||||||
|
gen "es" "My-mangas.com" "https://my-mangas.com"
|
||||||
|
gen "es" "SOS Scanlation" "https://sosscanlation.com"
|
||||||
|
# Went offline
|
||||||
|
# gen "fa" "TrinityReader" "http://trinityreader.pw"
|
||||||
|
gen "fr" "Manga-LEL" "https://www.manga-lel.com"
|
||||||
|
gen "fr" "Manga Etonnia" "https://www.etonnia.com"
|
||||||
|
gen "fr" "Scan FR" "http://www.scan-fr.net"
|
||||||
|
gen "fr" "ScanFR.com" "http://scanfr.com"
|
||||||
|
gen "fr" "Manga FYI" "http://mangafyi.com/manga/french"
|
||||||
|
gen "fr" "Mugiwara" "http://mugiwara.be"
|
||||||
|
gen "fr" "scans-manga" "http://scans-manga.com"
|
||||||
|
# Went offline
|
||||||
|
# gen "fr" "Tous Vos Scans" "http://www.tous-vos-scans.com"
|
||||||
|
gen "id" "Manga Desu" "http://mangadesu.net"
|
||||||
|
# Went offline
|
||||||
|
# gen "id" "Komik Mangafire.ID" "http://go.mangafire.id"
|
||||||
|
gen "id" "MangaOnline" "http://mangaonline.web.id"
|
||||||
|
# Went offline
|
||||||
|
# gen "id" "MangaNesia" "https://manganesia.com"
|
||||||
|
gen "id" "Komikid" "http://www.komikid.com"
|
||||||
|
gen "id" "MangaID" "http://mangaid.co"
|
||||||
|
gen "id" "Manga Seru" "http://www.mangaseru.top"
|
||||||
|
gen "id" "Manga FYI" "http://mangafyi.com/manga/indonesian"
|
||||||
|
# Went offline
|
||||||
|
# gen "id" "Indo Manga Reader" "http://indomangareader.com"
|
||||||
|
# Some sort of anti-bot system
|
||||||
|
# gen "it" "Kingdom Italia Reader" "http://kireader.altervista.org"
|
||||||
|
# Went offline
|
||||||
|
# gen "ja" "IchigoBook" "http://ichigobook.com"
|
||||||
|
# Went offline
|
||||||
|
# gen "ja" "Mangaraw Online" "http://mangaraw.online"
|
||||||
|
gen "ja" "Mangazuki RAWS" "https://raws.mangazuki.co"
|
||||||
|
gen "ja" "MangaRAW" "https://www.mgraw.com"
|
||||||
|
gen "pl" "Candy Scans" "http://csreader.webd.pl"
|
||||||
|
gen "pl" "ToraScans" "http://torascans.pl"
|
||||||
|
gen "pt" "Comic Space" "https://www.comicspace.com.br"
|
||||||
|
gen "pt" "Mangás Yuri" "https://mangasyuri.net"
|
||||||
|
gen "ru" "NAKAMA" "http://nakama.ru"
|
||||||
|
# Went offline
|
||||||
|
# gen "ru" "AkaiYuhiMun team" "https://akaiyuhimun.ru/reader"
|
||||||
|
gen "tr" "MangAoi" "http://mangaoi.com"
|
||||||
|
gen "tr" "MangaHanta" "http://mangahanta.com"
|
||||||
|
gen "tr" "ManhuaTR" "http://www.manhua-tr.com"
|
||||||
|
gen "vi" "Fallen Angels Scans" "http://truyen.fascans.com"
|
||||||
|
# Blocks bots (like this one)
|
||||||
|
# gen "tr" "Epikmanga" "http://www.epikmanga.com"
|
||||||
|
# NOTE: THIS SOURCE CONTAINS A CUSTOM LANGUAGE SYSTEM (which will be ignored)!
|
||||||
|
gen "other" "HentaiShark" "http://www.hentaishark.com"
|
||||||
|
|
||||||
|
if ! [ "$OPT_LIST" = true ] ; then
|
||||||
|
# Remove last comma from output
|
||||||
|
QUEUED_SOURCES="$(stripLastComma "$QUEUED_SOURCES")"
|
||||||
|
# Format, minify and split JSON output into chunks of 5000 chars
|
||||||
|
OUTPUT="$(echo -e "$QUEUED_SOURCES\n]" | jq -c . | fold -s -w5000)"
|
||||||
|
# Write file header
|
||||||
|
echo -e "package eu.kanade.tachiyomi.extension.all.mmrcms\n" >> "$TARGET"
|
||||||
|
echo -e "// GENERATED FILE, DO NOT MODIFY!" >> "$TARGET"
|
||||||
|
echo -e "// Generated on $(date)\n" >> "$TARGET"
|
||||||
|
# Convert split lines into variables
|
||||||
|
COUNTER=0
|
||||||
|
CONCAT="val SOURCES: String get() = "
|
||||||
|
TOTAL_LINES="$(echo "$OUTPUT" | wc -l)"
|
||||||
|
while read -r line; do
|
||||||
|
COUNTER=$[$COUNTER +1]
|
||||||
|
VARNAME="MMRSOURCE_$COUNTER"
|
||||||
|
echo "private val $VARNAME = \"\"\"$line\"\"\"" >> "$TARGET"
|
||||||
|
CONCAT="$CONCAT$VARNAME"
|
||||||
|
if [ "$COUNTER" -ne "$TOTAL_LINES" ]; then
|
||||||
|
CONCAT="$CONCAT + "
|
||||||
|
fi
|
||||||
|
done <<< "$OUTPUT"
|
||||||
|
echo "$CONCAT" >> "$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect and warn about duplicate sources
|
||||||
|
DUPES="$(echo -e "$PROCESSED" | sort | uniq -d)"
|
||||||
|
if [[ ! -z "$DUPES" ]]; then
|
||||||
|
echo
|
||||||
|
echo "----> WARNING, DUPLICATE SOURCES DETECTED! <----"
|
||||||
|
echo "Listing duplicates:"
|
||||||
|
echo "$DUPES"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done!"
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,286 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mmrcms
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MyMangaReaderCMSSource(override val lang: String,
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val supportsLatest: Boolean,
|
||||||
|
private val itemUrl: String,
|
||||||
|
private val categoryMappings: List<Pair<String, String>>,
|
||||||
|
private val tagMappings: List<Pair<String, String>>?) : HttpSource() {
|
||||||
|
private val jsonParser = JsonParser()
|
||||||
|
private val itemUrlPath = Uri.parse(itemUrl).pathSegments.first()
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/filterList?page=$page&sortBy=views&asc=false")
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
//Query overrides everything
|
||||||
|
val url: Uri.Builder
|
||||||
|
if(query.isNotBlank()) {
|
||||||
|
url = Uri.parse("$baseUrl/search")!!.buildUpon()
|
||||||
|
url.appendQueryParameter("query", query)
|
||||||
|
} else {
|
||||||
|
url = Uri.parse("$baseUrl/filterList?page=$page")!!.buildUpon()
|
||||||
|
filters.filterIsInstance<UriFilter>()
|
||||||
|
.forEach { it.addToUri(url) }
|
||||||
|
}
|
||||||
|
return GET(url.toString())
|
||||||
|
}
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filterList?page=$page&sortBy=last_release&asc=false")
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) = internalMangaParse(response)
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
return if(response.request().url().queryParameter("query")?.isNotBlank() == true) {
|
||||||
|
//If a search query was specified, use search instead!
|
||||||
|
MangasPage(jsonParser
|
||||||
|
.parse(response.body()!!.string())["suggestions"].array
|
||||||
|
.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
val segment = it["data"].string
|
||||||
|
setUrlWithoutDomain(itemUrl + segment)
|
||||||
|
title = it["value"].string
|
||||||
|
|
||||||
|
// Guess thumbnails
|
||||||
|
thumbnail_url = "$baseUrl/uploads/manga/$segment/cover/cover_250x350.jpg"
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
} else {
|
||||||
|
internalMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun latestUpdatesParse(response: Response) = internalMangaParse(response)
|
||||||
|
|
||||||
|
private fun internalMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return MangasPage(document.getElementsByClass("col-sm-6").map {
|
||||||
|
SManga.create().apply {
|
||||||
|
val urlElement = it.getElementsByClass("chart-title")
|
||||||
|
setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
title = urlElement.text().trim()
|
||||||
|
thumbnail_url = it.select(".media-left img").attr("src")
|
||||||
|
|
||||||
|
// Guess thumbnails on broken websites
|
||||||
|
if (thumbnail_url?.isBlank() != false || thumbnail_url?.endsWith("no-image.png") != false) {
|
||||||
|
thumbnail_url = "$baseUrl/uploads/manga/${url.substringAfterLast('/')}/cover/cover_250x350.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, document.select(".pagination a[rel=next]").isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response:Response) = SManga.create().apply {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
title = document.getElementsByClass("widget-title").text().trim()
|
||||||
|
thumbnail_url = document.select(".row .img-responsive").attr("src")
|
||||||
|
description = document.select(".row .well p").text().trim()
|
||||||
|
|
||||||
|
var cur: String? = null
|
||||||
|
for(element in document.select(".row .dl-horizontal").select("dt,dd")) {
|
||||||
|
when(element.tagName()) {
|
||||||
|
"dt" -> cur = element.text().trim().toLowerCase()
|
||||||
|
"dd" -> when(cur) {
|
||||||
|
"author(s)",
|
||||||
|
"autor(es)",
|
||||||
|
"auteur(s)",
|
||||||
|
"著作",
|
||||||
|
"yazar(lar)",
|
||||||
|
"mangaka(lar)",
|
||||||
|
"pengarang/penulis",
|
||||||
|
"pengarang",
|
||||||
|
"penulis",
|
||||||
|
"autor",
|
||||||
|
"المؤلف",
|
||||||
|
"перевод" -> author = element.text()
|
||||||
|
|
||||||
|
"artist(s)",
|
||||||
|
"artiste(s)",
|
||||||
|
"sanatçi(lar)",
|
||||||
|
"artista(s)",
|
||||||
|
"artist(s)/ilustrator",
|
||||||
|
"الرسام",
|
||||||
|
"seniman" -> artist = element.text()
|
||||||
|
|
||||||
|
"categories",
|
||||||
|
"categorías",
|
||||||
|
"catégories",
|
||||||
|
"ジャンル",
|
||||||
|
"kategoriler",
|
||||||
|
"categorias",
|
||||||
|
"kategorie",
|
||||||
|
"التصنيفات",
|
||||||
|
"жанр",
|
||||||
|
"kategori" -> genre = element.getElementsByTag("a").joinToString {
|
||||||
|
it.text().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
"status",
|
||||||
|
"statut",
|
||||||
|
"estado",
|
||||||
|
"状態",
|
||||||
|
"durum",
|
||||||
|
"الحالة",
|
||||||
|
"статус" -> status = when(element.text().trim().toLowerCase()) {
|
||||||
|
"complete",
|
||||||
|
"مكتملة",
|
||||||
|
"complet" -> SManga.COMPLETED
|
||||||
|
"ongoing",
|
||||||
|
"مستمرة",
|
||||||
|
"en cours" -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of chapters.
|
||||||
|
*
|
||||||
|
* Overriden to allow for null chapters
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select(chapterListSelector()).mapNotNull { nullableChapterFromElement(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||||
|
*/
|
||||||
|
fun chapterListSelector() = ".chapters > li:not(.btn)"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a chapter from the given element.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [chapterListSelector].
|
||||||
|
*/
|
||||||
|
private fun nullableChapterFromElement(element: Element): SChapter? {
|
||||||
|
val titleWrapper = element.getElementsByClass("chapter-title-rtl").first()
|
||||||
|
val url = titleWrapper.getElementsByTag("a").attr("href")
|
||||||
|
|
||||||
|
// Ensure chapter actually links to a manga
|
||||||
|
// Some websites use the chapters box to link to post announcements
|
||||||
|
if (!Uri.parse(url).pathSegments.firstOrNull().equals(itemUrlPath, true)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
|
||||||
|
chapter.setUrlWithoutDomain(url)
|
||||||
|
chapter.name = titleWrapper.text()
|
||||||
|
|
||||||
|
// Parse date
|
||||||
|
val dateText = element.getElementsByClass("date-chapter-title-rtl").text().trim()
|
||||||
|
val formattedDate = try {
|
||||||
|
DATE_FORMAT.parse(dateText).time
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
chapter.date_upload = formattedDate
|
||||||
|
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response)
|
||||||
|
= response.asJsoup().select("#all > .img-responsive")
|
||||||
|
.mapIndexed { i, e ->
|
||||||
|
val url = e.attr("data-src").trim()
|
||||||
|
Page(i, url, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response)
|
||||||
|
= throw UnsupportedOperationException("Unused method called!")
|
||||||
|
|
||||||
|
private fun getInitialFilterList() = listOf<Filter<*>>(
|
||||||
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
AuthorFilter(),
|
||||||
|
UriSelectFilter("Category",
|
||||||
|
"cat",
|
||||||
|
arrayOf("" to "Any",
|
||||||
|
*categoryMappings.toTypedArray()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
UriSelectFilter("Begins with",
|
||||||
|
"alpha",
|
||||||
|
arrayOf("" to "Any",
|
||||||
|
*"#ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray().map {
|
||||||
|
Pair(it.toString(), it.toString())
|
||||||
|
}.toTypedArray()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
UriSelectFilter("Sort by",
|
||||||
|
"sortBy",
|
||||||
|
arrayOf(
|
||||||
|
"name" to "Name",
|
||||||
|
"views" to "Popularity",
|
||||||
|
"last_release" to "Last update"
|
||||||
|
), false),
|
||||||
|
UriSelectFilter("Sort direction",
|
||||||
|
"asc",
|
||||||
|
arrayOf(
|
||||||
|
"true" to "Ascending",
|
||||||
|
"false" to "Descending"
|
||||||
|
), false)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
if(tagMappings != null)
|
||||||
|
(getInitialFilterList() + UriSelectFilter("Tag",
|
||||||
|
"tag",
|
||||||
|
arrayOf("" to "Any",
|
||||||
|
*tagMappings.toTypedArray()
|
||||||
|
)))
|
||||||
|
else getInitialFilterList()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||||
|
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||||
|
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||||
|
*/
|
||||||
|
//vals: <name, display>
|
||||||
|
open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||||
|
val firstIsUnspecified: Boolean = true,
|
||||||
|
defaultValue: Int = 0) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||||
|
override fun addToUri(uri: Uri.Builder) {
|
||||||
|
if (state != 0 || !firstIsUnspecified)
|
||||||
|
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthorFilter: Filter.Text("Author"), UriFilter {
|
||||||
|
override fun addToUri(uri: Uri.Builder) {
|
||||||
|
uri.appendQueryParameter("author", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a filter that is able to modify a URI.
|
||||||
|
*/
|
||||||
|
interface UriFilter {
|
||||||
|
fun addToUri(uri: Uri.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FORMAT = SimpleDateFormat("d MMM. yyyy", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mmrcms
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.bool
|
||||||
|
import com.github.salomonbrys.kotson.nullArray
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class MyMangaReaderCMSSources: SourceFactory {
|
||||||
|
/**
|
||||||
|
* Create a new copy of the sources
|
||||||
|
* @return The created sources
|
||||||
|
*/
|
||||||
|
override fun createSources() = parseSources(SOURCES)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a JSON array of sources into a list of `MyMangaReaderCMSSource`s
|
||||||
|
*
|
||||||
|
* Example JSON array:
|
||||||
|
* ```
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "language": "en",
|
||||||
|
* "name": "Example manga reader",
|
||||||
|
* "base_url": "http://example.com",
|
||||||
|
* "supports_latest": true,
|
||||||
|
* "item_url": "http://example.com/manga/",
|
||||||
|
* "categories": [
|
||||||
|
* {"id": "stuff", "name": "Stuff"},
|
||||||
|
* {"id": "test", "name": "Test"}
|
||||||
|
* ],
|
||||||
|
* "tags": [
|
||||||
|
* {"id": "action", "name": "Action"},
|
||||||
|
* {"id": "adventure", "name": "Adventure"}
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Sources that do not supports tags may use `null` instead of a list of json objects
|
||||||
|
*
|
||||||
|
* @param sourceString The JSON array of sources to parse
|
||||||
|
* @return The list of parsed sources
|
||||||
|
*/
|
||||||
|
private fun parseSources(sourceString: String): List<MyMangaReaderCMSSource> {
|
||||||
|
val parser = JsonParser()
|
||||||
|
val array = parser.parse(sourceString).array
|
||||||
|
|
||||||
|
return array.map {
|
||||||
|
it as JsonObject
|
||||||
|
|
||||||
|
val language = it["language"].string
|
||||||
|
val name = it["name"].string
|
||||||
|
val baseUrl = it["base_url"].string
|
||||||
|
val supportsLatest = it["supports_latest"].bool
|
||||||
|
val itemUrl = it["item_url"].string
|
||||||
|
val categories = mapToPairs(it["categories"].array)
|
||||||
|
val tags = it["tags"].nullArray?.let { mapToPairs(it) }
|
||||||
|
|
||||||
|
MyMangaReaderCMSSource(
|
||||||
|
language,
|
||||||
|
name,
|
||||||
|
baseUrl,
|
||||||
|
supportsLatest,
|
||||||
|
itemUrl,
|
||||||
|
categories,
|
||||||
|
tags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map an array of JSON objects to pairs. Each JSON object must have
|
||||||
|
* the following properties:
|
||||||
|
*
|
||||||
|
* id: first item in pair
|
||||||
|
* name: second item in pair
|
||||||
|
*
|
||||||
|
* @param array The array to process
|
||||||
|
* @return The new list of pairs
|
||||||
|
*/
|
||||||
|
private fun mapToPairs(array: JsonArray): List<Pair<String, String>>
|
||||||
|
= array.map {
|
||||||
|
it as JsonObject
|
||||||
|
|
||||||
|
it["id"].string to it["name"].string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue