added dummy extension

This commit is contained in:
jmir1
2021-04-22 22:11:15 +02:00
parent ee098a3ea0
commit f45cd4397d
2192 changed files with 52 additions and 79571 deletions

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Bato.to'
pkgNameSuffix = 'all.batoto'
extClass = '.BatoToFactory'
extVersionCode = 7
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@ -1,518 +0,0 @@
package eu.kanade.tachiyomi.extension.all.batoto
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
import java.util.concurrent.TimeUnit
open class BatoTo(
override val lang: String,
private val siteLang: String
) : ParsedHttpSource() {
override val name: String = "Bato.to"
override val baseUrl: String = "https://bato.to"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
}
override fun latestUpdatesSelector(): String {
return when (siteLang) {
"" -> "div#series-list div.col"
"en" -> "div#series-list div.col.no-flag"
else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])"
}
}
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val item = element.select("a.item-cover")
val imgurl = item.select("img").attr("abs:src")
manga.setUrlWithoutDomain(item.attr("href"))
manga.title = element.select("a.item-title").text()
manga.thumbnail_url = imgurl
return manga
}
override fun latestUpdatesNextPageSelector() = "div#mainer .pagination .page-item:not(.disabled) a.page-link:contains(»)"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=views_w&page=$page")
}
override fun popularMangaSelector() = latestUpdatesSelector()
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/search?word=$query&page=$page")
} else {
val url = HttpUrl.parse("$baseUrl/browse")!!.newBuilder()
url.addQueryParameter("page", page.toString())
url.addQueryParameter("langs", siteLang)
filters.forEach { filter ->
when (filter) {
is OriginFilter -> {
val originToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
originToInclude.add(content.name)
}
}
if (originToInclude.isNotEmpty()) {
url.addQueryParameter(
"origs",
originToInclude
.joinToString(",")
)
}
}
is StatusFilter -> {
if (filter.state != 0) {
url.addQueryParameter("release", filter.toUriPart())
}
}
is GenreFilter -> {
val genreToInclude = filter.state
.filter { it.isIncluded() }
.map { it.name }
val genreToExclude = filter.state
.filter { it.isExcluded() }
.map { it.name }
if (genreToInclude.isNotEmpty() || genreToExclude.isNotEmpty()) {
url.addQueryParameter(
"genres",
genreToInclude
.joinToString(",") +
"|" +
genreToExclude
.joinToString(",")
)
}
}
is ChapterFilter -> {
if (filter.state != 0) {
url.addQueryParameter("chapters", filter.toUriPart())
}
}
is SortBy -> {
if (filter.state != 0) {
url.addQueryParameter("sort", filter.toUriPart())
}
}
}
}
GET(url.build().toString(), headers)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.mangaDetailsRequest(manga)
}
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#mainer div.container-fluid")
val manga = SManga.create()
val genres = mutableListOf<String>()
val status = infoElement.select("div.attr-item:contains(status) span").text()
infoElement.select("div.attr-item:contains(genres) span").text().split(
" / "
.toRegex()
).forEach { element ->
genres.add(element)
}
manga.title = infoElement.select("h3").text()
manga.author = infoElement.select("div.attr-item:contains(author) a:first-child").text()
manga.artist = infoElement.select("div.attr-item:contains(author) a:last-child").text()
manga.status = parseStatus(status)
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
manga.description = infoElement.select("h5:contains(summary) + pre").text()
manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src")
return manga
}
private fun parseStatus(status: String?) = when {
status == null -> SManga.UNKNOWN
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.chapterListRequest(manga)
}
override fun chapterListSelector() = "div.main div.p-2"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
val urlElement = element.select("a.chapt")
val group = element.select("div.extra > a:not(.ps-3)").text()
val time = element.select("i").text()
.replace("a ", "1 ")
.replace("an ", "1 ")
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
if (group != "") {
chapter.scanlator = group
}
if (time != "") {
chapter.date_upload = parseChapterDate(time)
}
return chapter
}
private fun parseChapterDate(date: String): Long {
val value = date.split(' ')[0].toInt()
return when {
"secs" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"mins" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hours" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"days" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"weeks" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"months" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"years" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
"sec" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"min" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
else -> {
return 0
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("http")) {
return GET(chapter.url, headers)
}
return super.pageListRequest(chapter)
}
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
val script = document.select("script").html()
if (script.contains("var images =")) {
val imgJson = JSONObject(script.substringAfter("var images = ").substringBefore(";"))
val imgNames = imgJson.names()
if (imgNames != null) {
for (i in 0 until imgNames.length()) {
val imgKey = imgNames.getString(i)
val imgUrl = imgJson.getString(imgKey)
pages.add(Page(i, "", imgUrl))
}
}
} else if (script.contains("const server =")) { // bato.to
val duktape = Duktape.create()
val encryptedServer = script.substringAfter("const server = ").substringBefore(";")
val batojs = duktape.evaluate(script.substringAfter("const batojs = ").substringBefore(";")).toString()
val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($encryptedServer, \"$batojs\").toString(CryptoJS.enc.Utf8);"
val server = duktape.evaluate(decryptScript).toString().replace("\"", "")
duktape.close()
val imgArray = JSONArray(script.substringAfter("const images = ").substringBefore(";"))
if (imgArray != null) {
if (script.contains("bato.to/images")) {
for (i in 0 until imgArray.length()) {
val imgUrl = imgArray.get(i)
pages.add(Page(i, "", "$imgUrl"))
}
} else {
for (i in 0 until imgArray.length()) {
val imgUrl = imgArray.get(i)
if (server.startsWith("http"))
pages.add(Page(i, "", "${server}$imgUrl"))
else
pages.add(Page(i, "", "https:${server}$imgUrl"))
}
}
}
}
return pages
}
private val cryptoJS by lazy {
client.newCall(
GET(
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
headers
)
).execute().body()!!.string()
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
private class OriginFilter(genres: List<Tag>) : Filter.Group<Tag>("Origin", genres)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
private class ChapterFilter : UriPartFilter(
"Chapters",
arrayOf(
Pair("<select>", ""),
Pair("1 ~ 9", "1-9"),
Pair("10 ~ 29", "10-29"),
Pair("30 ~ 99", "30-99"),
Pair("100 ~ 199", "100-199"),
Pair("200+", "200"),
Pair("100+", "100"),
Pair("50+", "50"),
Pair("10+", "10"),
Pair("1+", "1")
)
)
private class SortBy : UriPartFilter(
"Sorts By",
arrayOf(
Pair("<select>", ""),
Pair("A-Z", "title.az"),
Pair("Z-A", "title"),
Pair("Last Updated", "update"),
Pair("Oldest Updated", "updated.az"),
Pair("Newest Added", "create"),
Pair("Oldest Added", "create.az"),
Pair("Most Views Totally", "views_a"),
Pair("Most Views 365 days", "views_y"),
Pair("Most Views 30 days", "views_m"),
Pair("Most Views 7 days", "views_w"),
Pair("Most Views 24 hours", "views_d"),
Pair("Most Views 60 minutes", "views_h"),
Pair("Least Views Totally", "views_a.az"),
Pair("Least Views 365 days", "views_y.az"),
Pair("Least Views 30 days", "views_m.az"),
Pair("Least Views 7 days", "views_w.az"),
Pair("Least Views 24 hours", "views_d.az"),
Pair("Least Views 60 minutes", "views_h.az")
)
)
private class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("<select>", ""),
Pair("Pending", "pending"),
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
Pair("Hiatus", "hiatus"),
Pair("Cancelled", "cancelled")
)
)
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
ChapterFilter(),
SortBy(),
StatusFilter(),
OriginFilter(getOriginList()),
GenreFilter(getGenreList())
)
private fun getOriginList() = listOf(
Tag("my"),
Tag("ceb"),
Tag("zh"),
Tag("zh_hk"),
Tag("en"),
Tag("en_us"),
Tag("fil"),
Tag("id"),
Tag("it"),
Tag("ja"),
Tag("ko"),
Tag("ms"),
Tag("pt_br"),
Tag("th"),
Tag("vi")
)
private fun getGenreList() = listOf(
Genre("Artbook"),
Genre("Cartoon"),
Genre("Comic"),
Genre("Doujinshi"),
Genre("Imageset"),
Genre("Manga"),
Genre("Manhua"),
Genre("Manhwa"),
Genre("Webtoon"),
Genre("Western"),
Genre("Josei"),
Genre("Seinen"),
Genre("Shoujo"),
Genre("Shoujo_Ai"),
Genre("Shounen"),
Genre("Shounen_Ai"),
Genre("Yaoi"),
Genre("Yuri"),
Genre("Ecchi"),
Genre("Mature"),
Genre("Adult"),
Genre("Gore"),
Genre("Violence"),
Genre("Smut"),
Genre("Hentai"),
Genre("4_Koma"),
Genre("Action"),
Genre("Adaptation"),
Genre("Adventure"),
Genre("Aliens"),
Genre("Animals"),
Genre("Anthology"),
Genre("Comedy"),
Genre("Cooking"),
Genre("Crime"),
Genre("Crossdressing"),
Genre("Delinquents"),
Genre("Dementia"),
Genre("Demons"),
Genre("Drama"),
Genre("Fantasy"),
Genre("Fan_Colored"),
Genre("Full_Color"),
Genre("Game"),
Genre("Gender_Bender"),
Genre("Genderswap"),
Genre("Ghosts"),
Genre("Gyaru"),
Genre("Harem"),
Genre("Harlequin"),
Genre("Historical"),
Genre("Horror"),
Genre("Incest"),
Genre("Isekai"),
Genre("Kids"),
Genre("Loli"),
Genre("Lolicon"),
Genre("Magic"),
Genre("Magical_Girls"),
Genre("Martial_Arts"),
Genre("Mecha"),
Genre("Medical"),
Genre("Military"),
Genre("Monster_Girls"),
Genre("Monsters"),
Genre("Music"),
Genre("Mystery"),
Genre("Netorare"),
Genre("Ninja"),
Genre("Office_Workers"),
Genre("Oneshot"),
Genre("Parody"),
Genre("Philosophical"),
Genre("Police"),
Genre("Post_Apocalyptic"),
Genre("Psychological"),
Genre("Reincarnation"),
Genre("Reverse_Harem"),
Genre("Romance"),
Genre("Samurai"),
Genre("School_Life"),
Genre("Sci_Fi"),
Genre("Shota"),
Genre("Shotacon"),
Genre("Slice_Of_Life"),
Genre("SM_BDSM"),
Genre("Space"),
Genre("Sports"),
Genre("Super_Power"),
Genre("Superhero"),
Genre("Supernatural"),
Genre("Survival"),
Genre("Thriller"),
Genre("Time_Travel"),
Genre("Tragedy"),
Genre("Vampires"),
Genre("Video_Games"),
Genre("Virtual_Reality"),
Genre("Wuxia"),
Genre("Xianxia"),
Genre("Xuanhuan"),
Genre("Zombies"),
Genre("award_winning"),
Genre("youkai"),
Genre("uncategorized")
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private class Tag(name: String) : Filter.CheckBox(name)
private class Genre(name: String) : Filter.TriState(name)
}

View File

@ -1,60 +0,0 @@
package eu.kanade.tachiyomi.extension.all.batoto
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class BatoToFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { BatoTo(it.first, it.second) }
}
private val languages = listOf(
//commented langueges do currently not exist on Bato.to but haven in the past
Pair("all",""),
Pair("ar", "ar"),
Pair("bg", "bg"),
Pair("cs", "cs"),
Pair("da", "da"),
Pair("de", "de"),
Pair("el", "el"),
Pair("en", "en"),
Pair("en-US", "en_us"),
Pair("es", "es"),
Pair("es-419", "es_419"),
Pair("eu", "eu"),
Pair("fa", "fa"),
Pair("fi", "fi"),
Pair("fil", "fil"),
Pair("fr", "fr"),
Pair("he", "he"),
//Pair("hi", "hi"),
Pair("hr", "hr"),
Pair("hu", "hu"),
Pair("id", "id"),
Pair("it", "it"),
Pair("ja", "ja"),
Pair("ko", "ko"),
//Pair("ku", "ku"),
Pair("ml", "ml"),
Pair("mn", "mn"),
Pair("ms", "ms"),
Pair("my", "my"),
Pair("nl", "nl"),
Pair("no", "no"),
Pair("pl", "pl"),
Pair("pt", "pt"),
Pair("pt-BR", "pt_br"),
Pair("pt-PT", "pt_pt"),
Pair("ro", "ro"),
Pair("ru", "ru"),
Pair("th", "th"),
Pair("tr", "tr"),
Pair("uk", "uk"),
Pair("vi", "vi"),
//Pair("xh", "xh"),
Pair("zh", "zh"),
Pair("zh-rHK", "zh_hk"),
Pair("zh-rTW", "zh_tw"),
Pair("zu", "zu"),
)

View File

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.cubari.CubariUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<!-- We need another intent filter so the /a/..* shortcut -->
<!-- doesn't pollute the cubari one, since they work in any combination -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*cubari.moe"
android:pathPattern="/read/..*"
android:scheme="https" />
<data
android:host="*cubari.moe"
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*guya.moe"
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*imgur.com"
android:pathPattern="/a/..*"
android:scheme="https" />
<data
android:host="*imgur.com"
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Cubari'
pkgNameSuffix = "all.cubari"
extClass = '.CubariFactory'
extVersionCode = 2
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,354 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.os.Build
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import rx.Observable
open class Cubari(override val lang: String) : HttpSource() {
final override val name = "Cubari"
final override val baseUrl = "https://cubari.moe"
final override val supportsLatest = true
override fun headersBuilder() = Headers.Builder().apply {
add(
"User-Agent",
"(Android ${Build.VERSION.RELEASE}; " +
"${Build.MANUFACTURER} ${Build.MODEL}) " +
"Tachiyomi/${BuildConfig.VERSION_NAME} " +
Build.ID
)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()!!
.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response -> latestUpdatesParse(response) }
}
override fun latestUpdatesParse(response: Response): MangasPage {
return parseMangaList(JSONArray(response.body()!!.string()), SortType.UNPINNED)
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()!!
.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response -> popularMangaParse(response) }
}
override fun popularMangaParse(response: Response): MangasPage {
return parseMangaList(JSONArray(response.body()!!.string()), SortType.PINNED)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response -> mangaDetailsParse(response, manga) }
}
// Called when the series is loaded, or when opening in browser
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
throw Exception("Unused")
}
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
return parseMangaFromApi(JSONObject(response.body()!!.string()), manga)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response -> chapterListParse(response, manga) }
}
// Gets the chapter list based on the series being viewed
override fun chapterListRequest(manga: SManga): Request {
val urlComponents = manga.url.split("/")
val source = urlComponents[2]
val slug = urlComponents[3]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
throw Exception("Unused")
}
// Called after the request
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val res = response.body()!!.string()
return parseChapterList(res, manga)
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return when {
chapter.url.contains("/chapter/") -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
directPageListParse(response)
}
}
else -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
seriesJsonPageListParse(response, chapter)
}
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
return when {
chapter.url.contains("/chapter/") -> {
GET("$baseUrl${chapter.url}", headers)
}
else -> {
var url = chapter.url.split("/")
val source = url[2]
val slug = url[3]
GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
}
}
private fun directPageListParse(response: Response): List<Page> {
val res = response.body()!!.string()
val pages = JSONArray(res)
val pageArray = ArrayList<Page>()
for (i in 0 until pages.length()) {
val page = if (pages.optJSONObject(i) != null) {
pages.getJSONObject(i).getString("src")
} else {
pages[i]
}
pageArray.add(Page(i + 1, "", page.toString()))
}
return pageArray
}
private fun seriesJsonPageListParse(response: Response, chapter: SChapter): List<Page> {
val res = response.body()!!.string()
val json = JSONObject(res)
val groups = json.getJSONObject("groups")
val groupIter = groups.keys()
val groupMap = HashMap<String, String>()
while (groupIter.hasNext()) {
val groupKey = groupIter.next()
groupMap[groups.getString(groupKey)] = groupKey
}
val chapters = json.getJSONObject("chapters")
val pages = if (chapters.has(chapter.chapter_number.toString())) {
chapters
.getJSONObject(chapter.chapter_number.toString())
.getJSONObject("groups")
.getJSONArray(groupMap[chapter.scanlator])
} else {
chapters
.getJSONObject(chapter.chapter_number.toInt().toString())
.getJSONObject("groups")
.getJSONArray(groupMap[chapter.scanlator])
}
val pageArray = ArrayList<Page>()
for (i in 0 until pages.length()) {
val page = if (pages.optJSONObject(i) != null) {
pages.getJSONObject(i).getString("src")
} else {
pages[i]
}
pageArray.add(Page(i + 1, "", page.toString()))
}
return pageArray
}
// Stub
override fun pageListParse(response: Response): List<Page> {
throw Exception("Unused")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PROXY_PREFIX) -> {
val trimmedQuery = query.removePrefix(PROXY_PREFIX)
// Only tag for recently read on search
client.newBuilder()
.addInterceptor(RemoteStorageUtils.TagInterceptor())
.build()!!
.newCall(searchMangaRequest(page, trimmedQuery, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, trimmedQuery)
}
}
else -> throw Exception(SEARCH_FALLBACK_MSG)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
try {
val queryFragments = query.split("/")
val source = queryFragments[0]
val slug = queryFragments[1]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
} catch (e: Exception) {
throw Exception(SEARCH_FALLBACK_MSG)
}
}
override fun searchMangaParse(response: Response): MangasPage {
throw Exception("Unused")
}
private fun searchMangaParse(response: Response, query: String): MangasPage {
return parseSearchList(JSONObject(response.body()!!.string()), query)
}
// ------------- Helpers and whatnot ---------------
private fun parseChapterList(payload: String, manga: SManga): List<SChapter> {
val json = JSONObject(payload)
val groups = json.getJSONObject("groups")
val chapters = json.getJSONObject("chapters")
val chapterList = ArrayList<SChapter>()
val iter = chapters.keys()
while (iter.hasNext()) {
val chapterNum = iter.next()
val chapterObj = chapters.getJSONObject(chapterNum)
val chapterGroups = chapterObj.getJSONObject("groups")
val groupsIter = chapterGroups.keys()
while (groupsIter.hasNext()) {
val groupNum = groupsIter.next()
val chapter = SChapter.create()
chapter.scanlator = groups.getString(groupNum)
if (chapterObj.has("release_date")) {
chapter.date_upload =
chapterObj.getJSONObject("release_date").getLong(groupNum) * 1000
}
chapter.name = chapterNum + " - " + chapterObj.getString("title")
chapter.chapter_number = chapterNum.toFloat()
chapter.url =
if (chapterGroups.optJSONArray(groupNum) != null) {
"${manga.url}/$chapterNum/$groupNum"
} else {
chapterGroups.getString(groupNum)
}
chapterList.add(chapter)
}
}
return chapterList.reversed()
}
private fun parseMangaList(payload: JSONArray, sortType: SortType): MangasPage {
val mangas = ArrayList<SManga>()
for (i in 0 until payload.length()) {
val json = payload.getJSONObject(i)
val pinned = json.getBoolean("pinned")
if (sortType == SortType.PINNED && pinned) {
mangas.add(parseMangaFromRemoteStorage(json))
} else if (sortType == SortType.UNPINNED && !pinned) {
mangas.add(parseMangaFromRemoteStorage(json))
}
}
return MangasPage(mangas, false)
}
private fun parseSearchList(payload: JSONObject, query: String): MangasPage {
val mangas = ArrayList<SManga>()
val tempManga = SManga.create()
tempManga.url = "/read/$query"
mangas.add(parseMangaFromApi(payload, tempManga))
return MangasPage(mangas, false)
}
private fun parseMangaFromRemoteStorage(json: JSONObject): SManga {
val manga = SManga.create()
manga.title = json.getString("title")
manga.artist = json.optString("artist", ARTIST_FALLBACK)
manga.author = json.optString("author", AUTHOR_FALLBACK)
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
manga.url = json.getString("url")
manga.thumbnail_url = json.getString("coverUrl")
return manga
}
private fun parseMangaFromApi(json: JSONObject, mangaReference: SManga): SManga {
val manga = SManga.create()
manga.title = json.getString("title")
manga.artist = json.optString("artist", ARTIST_FALLBACK)
manga.author = json.optString("author", AUTHOR_FALLBACK)
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
manga.url = mangaReference.url
manga.thumbnail_url = json.optString("cover", "")
return manga
}
// ----------------- Things we aren't supporting -----------------
override fun imageUrlParse(response: Response): String {
throw Exception("imageUrlParse not supported.")
}
companion object {
const val PROXY_PREFIX = "cubari:"
const val AUTHOR_FALLBACK = "Unknown"
const val ARTIST_FALLBACK = "Unknown"
const val DESCRIPTION_FALLBACK = "No description."
const val SEARCH_FALLBACK_MSG = "Unable to parse. Is your query in the format of $PROXY_PREFIX<source>/<slug>?"
enum class SortType {
PINNED,
UNPINNED
}
}
}

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class CubariFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Cubari("en"),
Cubari("all"),
Cubari("other")
)
}

View File

@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class CubariUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = intent?.data?.host
val pathSegments = intent?.data?.pathSegments
if (host != null && pathSegments != null) {
val query = when (host) {
"m.imgur.com", "imgur.com" -> fromImgur(pathSegments)
else -> fromCubari(pathSegments)
}
if (query == null) {
Log.e("CubariUrlActivity", "Unable to parse URI from intent $intent")
finish()
exitProcess(1)
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("CubariUrlActivity", e.toString())
}
}
finish()
exitProcess(0)
}
private fun fromImgur(pathSegments: List<String>): String? {
if (pathSegments.size >= 2) {
val id = pathSegments[1]
return "${Cubari.PROXY_PREFIX}imgur/$id"
}
return null
}
private fun fromCubari(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 3) {
val source = pathSegments[1]
val slug = pathSegments[2]
"${Cubari.PROXY_PREFIX}$source/$slug"
} else {
null
}
}
}

View File

@ -1,147 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class RemoteStorageUtils {
abstract class GenericInterceptor(private val transparent: Boolean) : Interceptor {
private val handler = Handler(Looper.getMainLooper())
abstract val jsScript: String
abstract fun urlModifier(originalUrl: String): String
internal class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
try {
val originalRequest = chain.request()
val originalResponse = chain.proceed(originalRequest)
return proceedWithWebView(originalRequest, originalResponse)
} catch (e: Exception) {
throw IOException(e)
}
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(request: Request, response: Response): Response {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url().toString()
val headers = request.headers().toMultimap().mapValues {
it.value.getOrNull(0) ?: ""
}.toMutableMap()
val jsInterface = JsInterface(latch)
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
}
webview.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
view.evaluateJavascript(jsScript) {}
}
if (transparent) {
latch.countDown()
}
}
}
webview.loadUrl(urlModifier(origRequestUrl), headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.postDelayed(
{ webView?.destroy() },
DELAY_MILLIS * (if (transparent) 2 else 1)
)
return if (transparent) {
response
} else {
response.newBuilder().body(ResponseBody.create(response.body()?.contentType(), jsInterface.payload)).build()
}
}
}
class TagInterceptor : GenericInterceptor(true) {
override val jsScript: String = """
let dispatched = false;
window.addEventListener('history-ready', function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e)))
});
}
});
tag();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl.replace("/api/", "/").replace("/series/", "/")
}
}
class HomeInterceptor : GenericInterceptor(false) {
override val jsScript: String = """
let dispatched = false;
(function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e) ) )
});
}
})();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl
}
}
companion object {
const val TIMEOUT_SEC: Long = 10
const val DELAY_MILLIS: Long = 10000
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Dragon Ball Multiverse'
pkgNameSuffix = 'all.dragonball_multiverse'
extClass = '.DbMFactory'
extVersionCode = 2
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

View File

@ -1,83 +0,0 @@
@file:Suppress("ClassName")
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class DbMFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
DbMultiverseEN(),
DbMultiverseFR(),
DbMultiverseJP(),
DbMultiverseCN(),
DbMultiverseES(),
DbMultiverseIT(),
DbMultiversePT(),
DbMultiverseDE(),
DbMultiversePL(),
DbMultiverseNL(),
DbMultiverseFR_PA(),
DbMultiverseTR_TR(),
DbMultiversePT_BR(),
DbMultiverseHU_HU(),
DbMultiverseGA_ES(),
DbMultiverseCT_CT(),
DbMultiverseNO_NO(),
DbMultiverseRU_RU(),
DbMultiverseRO_RO(),
DbMultiverseEU_EH(),
DbMultiverseLT_LT(),
DbMultiverseHR_HR(),
DbMultiverseKR_KR(),
DbMultiverseFI_FI(),
DbMultiverseHE_HE(),
DbMultiverseBG_BG(),
DbMultiverseSV_SE(),
DbMultiverseGR_GR(),
DbMultiverseES_CO(),
DbMultiverseAR_JO(),
DbMultiverseTL_PI(),
DbMultiverseLA_LA(),
DbMultiverseDA_DK(),
DbMultiverseCO_FR(),
DbMultiverseBR_FR(),
DbMultiverseXX_VE()
)
}
class DbMultiverseFR : DbMultiverse("fr", "fr")
class DbMultiverseJP : DbMultiverse("ja", "jp")
class DbMultiverseCN : DbMultiverse("zh", "cn")
class DbMultiverseES : DbMultiverse("es", "es")
class DbMultiverseIT : DbMultiverse("it", "it")
class DbMultiversePT : DbMultiverse("pt", "pt")
class DbMultiverseDE : DbMultiverse("de", "de")
class DbMultiversePL : DbMultiverse("pl", "pl")
class DbMultiverseNL : DbMultiverse("nl", "nl")
class DbMultiverseFR_PA : DbMultiverse("fr", "fr_PA")
class DbMultiverseTR_TR : DbMultiverse("tr", "tr_TR")
class DbMultiversePT_BR : DbMultiverse("pt-BR", "pt_BR")
class DbMultiverseHU_HU : DbMultiverse("hu", "hu_HU")
class DbMultiverseGA_ES : DbMultiverse("ga", "ga_ES")
class DbMultiverseCT_CT : DbMultiverse("ca", "ct_CT")
class DbMultiverseNO_NO : DbMultiverse("no", "no_NO")
class DbMultiverseRU_RU : DbMultiverse("ru", "ru_RU")
class DbMultiverseRO_RO : DbMultiverse("ro", "ro_RO")
class DbMultiverseEU_EH : DbMultiverse("eu", "eu_EH")
class DbMultiverseLT_LT : DbMultiverse("lt", "lt_LT")
class DbMultiverseHR_HR : DbMultiverse("hr", "hr_HR")
class DbMultiverseKR_KR : DbMultiverse("ko", "kr_KR")
class DbMultiverseFI_FI : DbMultiverse("fi", "fi_FI")
class DbMultiverseHE_HE : DbMultiverse("he", "he_HE")
class DbMultiverseBG_BG : DbMultiverse("bg", "bg_BG")
class DbMultiverseSV_SE : DbMultiverse("sv", "sv_SE")
class DbMultiverseGR_GR : DbMultiverse("el", "gr_GR")
class DbMultiverseES_CO : DbMultiverse("es-419", "es_CO")
class DbMultiverseAR_JO : DbMultiverse("ar", "ar_JO")
class DbMultiverseTL_PI : DbMultiverse("fil", "tl_PI")
class DbMultiverseLA_LA : DbMultiverse("la", "la_LA")
class DbMultiverseDA_DK : DbMultiverse("da", "da_DK")
class DbMultiverseCO_FR : DbMultiverse("co", "co_FR")
class DbMultiverseBR_FR : DbMultiverse("br", "br_FR")
class DbMultiverseXX_VE : DbMultiverse("vec", "xx_VE")

View File

@ -1,103 +0,0 @@
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
abstract class DbMultiverse(override val lang: String, private val internalLang: String) : ParsedHttpSource() {
override val name =
if (internalLang.endsWith("_PA")) "Dragon Ball Multiverse Parody"
else "Dragon Ball Multiverse"
override val baseUrl = "https://www.dragonball-multiverse.com"
override val supportsLatest = false
private fun chapterFromElement(element: Element, name: String): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.attr("abs:href"))
chapter.name = name + element.text().let { num ->
if (num.contains("-")) {
"Pages $num"
} else {
"Page $num"
}
}
return chapter
}
override fun chapterListSelector(): String = "div.cadrelect.chapters a[href*=page-]"
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = mutableListOf<SChapter>()
val document = response.asJsoup()
document.select("div[ch]").forEach { container ->
container.select(chapterListSelector()).mapIndexed { i, chapter ->
// Each page is its own chapter, add chapter name when a first page is mapped
val name = if (i == 0) container.select("h4").text() + " - " else ""
chapters.add(chapterFromElement(chapter, name))
}
}
return chapters.reversed()
}
override fun pageListParse(document: Document): List<Page> {
return document.select("div#h_read img").mapIndexed { index, element ->
Page(index, "", element.attr("abs:src"))
}
}
override fun mangaDetailsParse(document: Document): SManga = createManga(document)
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.just(MangasPage(listOf(createManga(null)), hasNextPage = false))
}
private fun createManga(document: Document?) = SManga.create().apply {
title = name
status = SManga.ONGOING
url = "/$internalLang/chapters.html"
description = "Dragon Ball Multiverse (DBM) is a free online comic, made by a whole team of fans. It's our personal sequel to DBZ."
thumbnail_url = document?.select("div[ch=\"1\"] img")?.attr("abs:src")
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
override fun popularMangaSelector(): String = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector(): String? = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.just(MangasPage(emptyList(), false))
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
}

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
class DbMultiverseEN : DbMultiverse("en", "en")

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.ehentai.EHUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="e-hentai.org"
android:pathPattern="/g/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'E-Hentai'
pkgNameSuffix = 'all.ehentai'
extClass = '.EHFactory'
extVersionCode = 13
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class EHFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
EHentai("ja", "japanese"),
EHentai("en", "english"),
EHentai("zh", "chinese"),
EHentai("nl", "dutch"),
EHentai("fr", "french"),
EHentai("de", "german"),
EHentai("hu", "hungarian"),
EHentai("it", "italian"),
EHentai("ko", "korean"),
EHentai("pl", "polish"),
EHentai("pt", "portuguese"),
EHentai("ru", "russian"),
EHentai("es", "spanish"),
EHentai("th", "thai"),
EHentai("vi", "vietnamese"),
EHentai("none", "n/a"),
EHentai("other", "other")
)
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://e-hentai.net/g/xxxxx/yyyyy/ intents and redirects them to
* the main Tachiyomi process.
*/
class EHUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 2) {
val id = pathSegments[1]
val key = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${EHentai.PREFIX_ID_SEARCH}$id/$key")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("EHUrlActivity", e.toString())
}
} else {
Log.e("EHUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import kotlin.math.ln
import kotlin.math.pow
/**
* Various utility methods used in the E-Hentai source
*/
/**
* Return null if String is blank, otherwise returns the original String
* @returns null if the String is blank, otherwise returns the original String
*/
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
null
else
this
/**
* Ignores any exceptions thrown inside a block
*/
fun <T> ignore(expr: () -> T): T? {
return try {
expr()
} catch (t: Throwable) {
null
}
}
/**
* Use '+' to append Strings onto a StringBuilder
*/
operator fun StringBuilder.plusAssign(other: String) {
append(other)
}
/**
* Converts bytes into a human readable String
*/
fun humanReadableByteCount(bytes: Long, si: Boolean): String {
val unit = if (si) 1000 else 1024
if (bytes < unit) return "$bytes B"
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}
private const val KB_FACTOR = 1000
private const val KIB_FACTOR = 1024
private const val MB_FACTOR = 1000 * KB_FACTOR
private const val MIB_FACTOR = 1024 * KIB_FACTOR
private const val GB_FACTOR = 1000 * MB_FACTOR
private const val GIB_FACTOR = 1024 * MIB_FACTOR
/**
* Parse human readable size Strings
*/
fun parseHumanReadableByteCount(arg0: String): Double? {
val spaceNdx = arg0.indexOf(" ")
val ret = arg0.substring(0 until spaceNdx).toDouble()
when (arg0.substring(spaceNdx + 1)) {
"GB" -> return ret * GB_FACTOR
"GiB" -> return ret * GIB_FACTOR
"MB" -> return ret * MB_FACTOR
"MiB" -> return ret * MIB_FACTOR
"KB" -> return ret * KB_FACTOR
"KiB" -> return ret * KIB_FACTOR
}
return null
}

View File

@ -1,533 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.Filter.CheckBox
import eu.kanade.tachiyomi.source.model.Filter.Group
import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Text
import eu.kanade.tachiyomi.source.model.Filter.TriState
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URLEncoder
import android.support.v7.preference.CheckBoxPreference as LegacyCheckBoxPreference
import android.support.v7.preference.PreferenceScreen as LegacyPreferenceScreen
open class EHentai(override val lang: String, private val ehLang: String) : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val name = "E-Hentai"
override val baseUrl = "https://e-hentai.org"
override val supportsLatest = true
// true if lang is a "natural human language"
private fun isLangNatural(): Boolean = lang !in listOf("none", "other")
private fun genericMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val parsedMangas = doc.select("table.itg td.glname")
.let { elements ->
if (isLangNatural() && getEnforceLanguagePref()) {
elements.filter { element ->
// only accept elements with a language tag matching ehLang or without a language tag
// could make this stricter and not accept elements without a language tag, possibly add a sharedpreference for it
element.select("div[title^=language]").firstOrNull()?.let { it.text() == ehLang } ?: true
}
} else {
elements
}
}
.map {
SManga.create().apply {
// Get title
it.select("a")?.first()?.apply {
title = this.select(".glink").text()
url = ExGalleryMetadata.normalizeUrl(attr("href"))
}
// Get image
it.parent().select(".glthumb img")?.first().apply {
thumbnail_url = this?.attr("data-src")?.nullIfBlank()
?: this?.attr("src")
}
}
}
// Add to page if required
val hasNextPage = doc.select("a[onclick=return false]").last()?.text() == ">"
return MangasPage(parsedMangas, hasNextPage)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 1f
}
)
)
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, "$baseUrl/${chapter.url}").map {
it.mapIndexed { i, s ->
Page(i, s)
}
}!!
/**
* Recursively fetch chapter pages
*/
private fun fetchChapterPage(
chapter: SChapter,
np: String,
pastUrls: List<String> = emptyList()
): Observable<List<String>> {
val urls = ArrayList(pastUrls)
return chapterPageCall(np).flatMap {
val jsoup = it.asJsoup()
urls += parseChapterPage(jsoup)
nextPageUrl(jsoup)?.let { string ->
fetchChapterPage(chapter, string, urls)
} ?: Observable.just(urls)
}
}
private fun parseChapterPage(response: Element) = with(response) {
select(".gdtm a").map {
Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
}.sortedBy(Pair<Int, String>::first).map { it.second }
}
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
private fun chapterPageRequest(np: String) = exGet(np, null, headers)
private fun nextPageUrl(element: Element) = element.select("a[onclick=return false]").last()?.let {
if (it.text() == ">") it.attr("href") else null
}
private fun languageTag(enforceLanguageFilter: Boolean = false): String {
return if (enforceLanguageFilter || getEnforceLanguagePref()) "language:$ehLang" else ""
}
override fun popularMangaRequest(page: Int) = if (isLangNatural()) {
exGet("$baseUrl/?f_search=${languageTag()}&f_srdd=5&f_sr=on", page)
} else {
latestUpdatesRequest(page)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val enforceLanguageFilter = filters.find { it is EnforceLanguageFilter }?.state == true
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
var modifiedQuery = when {
!isLangNatural() -> query
query.isBlank() -> languageTag(enforceLanguageFilter)
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
}
modifiedQuery += filters.filterIsInstance<TagFilter>()
.flatMap { it.markedTags() }
.joinToString(",")
.let { if (it.isNotEmpty()) ",$it" else it }
uri.appendQueryParameter("f_search", modifiedQuery)
filters.forEach {
if (it is UriFilter) it.addToUri(uri)
}
return exGet(uri.toString(), page)
}
override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)
override fun popularMangaParse(response: Response) = genericMangaParse(response)
override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
return GET(
page?.let {
addParam(url, "page", (page - 1).toString())
} ?: url,
additionalHeaders?.let { header ->
val headers = headers.newBuilder()
header.toMultimap().forEach { (t, u) ->
u.forEach {
headers.add(t, it)
}
}
headers.build()
} ?: headers
).let {
if (!cache) {
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
} else {
it
}
}
}
/**
* Parse gallery page to metadata model
*/
@SuppressLint("DefaultLocale")
override fun mangaDetailsParse(response: Response) = with(response.asJsoup()) {
with(ExGalleryMetadata()) {
url = response.request().url().encodedPath()
title = select("#gn").text().nullIfBlank()?.trim()
altTitle = select("#gj").text().nullIfBlank()?.trim()
// Thumbnail is set as background of element in style attribute
thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
}
genre = select("#gdc div").text().nullIfBlank()?.trim()?.toLowerCase()
uploader = select("#gdn").text().nullIfBlank()?.trim()
// Parse the table
select("#gdd tr").forEach {
it.select(".gdt1")
.text()
.nullIfBlank()
?.trim()
?.let { left ->
it.select(".gdt2")
.text()
.nullIfBlank()
?.trim()
?.let { right ->
ignore {
when (
left.removeSuffix(":")
.toLowerCase()
) {
"posted" -> datePosted = EX_DATE_FORMAT.parse(right)?.time ?: 0
"visible" -> visible = right.nullIfBlank()
"language" -> {
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
translated = right.endsWith(TR_SUFFIX, true)
}
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
}
}
}
}
}
// Parse ratings
ignore {
averageRating = select("#rating_label")
.text()
.removePrefix("Average:")
.trim()
.nullIfBlank()
?.toDouble()
ratingCount = select("#rating_count")
.text()
.trim()
.nullIfBlank()
?.toInt()
}
// Parse tags
tags.clear()
select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":")
val currentTags = it.select("div").map { element ->
Tag(
element.text().trim(),
element.hasClass("gtl")
)
}
tags[namespace] = currentTags
}
// Copy metadata to manga
SManga.create().apply {
copyTo(this)
}
}
}
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id", headers)
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
val details = mangaDetailsParse(response)
details.url = "/g/$id/"
return MangasPage(listOf(details), false)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, id) }
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
override fun pageListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
override fun imageUrlParse(response: Response): String = response.asJsoup().select("#img").attr("abs:src")
private val cookiesHeader by lazy {
val cookies = mutableMapOf<String, String>()
// Setup settings
val settings = mutableListOf<String>()
// Do not show popular right now pane as we can't parse it
settings += "prn_n"
// Exclude every other language except the one we have selected
settings += "xl_" + languageMappings.filter { it.first != ehLang }
.flatMap { it.second }
.joinToString("x")
cookies["uconfig"] = buildSettings(settings)
// Bypass "Offensive For Everyone" content warning
cookies["nw"] = "1"
buildCookies(cookies)
}
// Headers
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader)!!
private fun buildSettings(settings: List<String?>) = settings.filterNotNull().joinToString(separator = "-")
private fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
@Suppress("SameParameterValue")
private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
.buildUpon()
.appendQueryParameter(param, value)
.toString()
override val client = network.client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES)
.addInterceptor { chain ->
val newReq = chain
.request()
.newBuilder()
.removeHeader("Cookie")
.addHeader("Cookie", cookiesHeader)
.build()
chain.proceed(newReq)
}.build()!!
// Filters
override fun getFilterList() = FilterList(
EnforceLanguageFilter(getEnforceLanguagePref()),
Watched(),
GenreGroup(),
TagFilter("Misc Tags", triStateBoxesFrom(miscTags), ""),
TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
AdvancedGroup()
)
class Watched : CheckBox("Watched List"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state)
builder.appendPath("watched")
}
}
class GenreOption(name: String, private val genreId: String) : CheckBox(name, false), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
}
}
class GenreGroup : UriGroup<GenreOption>(
"Genres",
listOf(
GenreOption("Dōjinshi", "doujinshi"),
GenreOption("Manga", "manga"),
GenreOption("Artist CG", "artistcg"),
GenreOption("Game CG", "gamecg"),
GenreOption("Western", "western"),
GenreOption("Non-H", "non-h"),
GenreOption("Image Set", "imageset"),
GenreOption("Cosplay", "cosplay"),
GenreOption("Asian Porn", "asianporn"),
GenreOption("Misc", "misc")
)
)
class AdvancedOption(name: String, private val param: String, defValue: Boolean = false) : CheckBox(name, defValue), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state)
builder.appendQueryParameter(param, "on")
}
}
open class PageOption(name: String, private val queryKey: String) : Text(name), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state.isNotBlank()) {
if (builder.build().getQueryParameters("f_sp").isEmpty()) {
builder.appendQueryParameter("f_sp", "on")
}
builder.appendQueryParameter(queryKey, state.trim())
}
}
}
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
class RatingOption :
Select<String>(
"Minimum Rating",
arrayOf(
"Any",
"2 stars",
"3 stars",
"4 stars",
"5 stars"
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state > 0) {
builder.appendQueryParameter("f_srdd", (state + 1).toString())
builder.appendQueryParameter("f_sr", "on")
}
}
}
// Explicit type arg for listOf() to workaround this: KT-16570
class AdvancedGroup : UriGroup<Filter<*>>(
"Advanced Options",
listOf(
AdvancedOption("Search Gallery Name", "f_sname", true),
AdvancedOption("Search Gallery Tags", "f_stags", true),
AdvancedOption("Search Gallery Description", "f_sdesc"),
AdvancedOption("Search Torrent Filenames", "f_storr"),
AdvancedOption("Only Show Galleries With Torrents", "f_sto"),
AdvancedOption("Search Low-Power Tags", "f_sdt1"),
AdvancedOption("Search Downvoted Tags", "f_sdt2"),
AdvancedOption("Show Expunged Galleries", "f_sh"),
RatingOption(),
MinPagesOption(),
MaxPagesOption()
)
)
private class EnforceLanguageFilter(default: Boolean) : CheckBox("Enforce language", default)
private val miscTags = "3d, already uploaded, anaglyph, animal on animal, animated, anthology, arisa mizuhara, artbook, ashiya noriko, bailey jay, body swap, caption, chouzuki maryou, christian godard, comic, compilation, dakimakura, fe galvao, ffm threesome, figure, forbidden content, full censorship, full color, game sprite, goudoushi, group, gunyou mikan, harada shigemitsu, hardcore, helly von valentine, higurashi rin, hololive, honey select, how to, incest, incomplete, ishiba yoshikazu, jessica nigri, kalinka fox, kanda midori, kira kira, kitami eri, kuroi hiroki, lenfried, lincy leaw, marie claude bourbonnais, matsunaga ayaka, me me me, missing cover, mmf threesome, mmt threesome, mosaic censorship, mtf threesome, multi-work series, no penetration, non-nude, novel, nudity only, oakazaki joe, out of order, paperchild, pm02 colon 20, poor grammar, radio comix, realporn, redraw, replaced, sakaki kasa, sample, saotome love, scanmark, screenshots, sinful goddesses, sketch lines, stereoscopic, story arc, takeuti ken, tankoubon, themeless, tikuma jukou, time stop, tsubaki zakuro, ttm threesome, twins, uncensored, vandych alex, variant set, watermarked, webtoon, western cg, western imageset, western non-h, yamato nadeshiko club, yui okada, yukkuri, zappa go"
private val femaleTags = "ahegao, anal, angel, apron, bandages, bbw, bdsm, beauty mark, big areolae, big ass, big breasts, big clit, big lips, big nipples, bikini, blackmail, bloomers, blowjob, bodysuit, bondage, breast expansion, bukkake, bunny girl, business suit, catgirl, centaur, cheating, chinese dress, christmas, collar, corset, cosplaying, cowgirl, crossdressing, cunnilingus, dark skin, daughter, deepthroat, defloration, demon girl, double penetration, dougi, dragon, drunk, elf, exhibitionism, farting, females only, femdom, filming, fingering, fishnets, footjob, fox girl, furry, futanari, garter belt, ghost, giantess, glasses, gloves, goblin, gothic lolita, growth, guro, gyaru, hair buns, hairy, hairy armpits, handjob, harem, hidden sex, horns, huge breasts, humiliation, impregnation, incest, inverted nipples, kemonomimi, kimono, kissing, lactation, latex, leg lock, leotard, lingerie, lizard girl, maid, masked face, masturbation, midget, miko, milf, mind break, mind control, monster girl, mother, muscle, nakadashi, netorare, nose hook, nun, nurse, oil, paizuri, panda girl, pantyhose, piercing, pixie cut, policewoman, ponytail, pregnant, rape, rimjob, robot, scat, schoolgirl uniform, sex toys, shemale, sister, small breasts, smell, sole dickgirl, sole female, squirting, stockings, sundress, sweating, swimsuit, swinging, tail, tall girl, teacher, tentacles, thigh high boots, tomboy, transformation, twins, twintails, unusual pupils, urination, vore, vtuber, widow, wings, witch, wolf girl, x-ray, yuri, zombie"
private val maleTags = "anal, bbm, big ass, big penis, bikini, blood, blowjob, bondage, catboy, cheating, chikan, condom, crab, crossdressing, dark skin, deepthroat, demon, dickgirl on male, dilf, dog boy, double anal, double penetration, dragon, drunk, exhibitionism, facial hair, feminization, footjob, fox boy, furry, glasses, group, guro, hairy, handjob, hidden sex, horns, huge penis, human on furry, kimono, lingerie, lizard guy, machine, maid, males only, masturbation, mmm threesome, monster, muscle, nakadashi, ninja, octopus, oni, pillory, policeman, possession, prostate massage, public use, schoolboy uniform, schoolgirl uniform, sex toys, shotacon, sleeping, snuff, sole male, stockings, sunglasses, swimsuit, tall man, tentacles, tomgirl, unusual pupils, virginity, waiter, x-ray, yaoi, zombie"
private fun triStateBoxesFrom(tagString: String): List<TagTriState> = tagString.split(", ").map { TagTriState(it) }
class TagTriState(tag: String) : TriState(tag)
class TagFilter(name: String, private val triStateBoxes: List<TagTriState>, private val nameSpace: String) : Group<TagTriState>(name, triStateBoxes) {
fun markedTags() = triStateBoxes.filter { it.isIncluded() }.map { "$nameSpace:${it.name}" } + triStateBoxes.filter { it.isExcluded() }.map { "-$nameSpace:${it.name}" }
}
// map languages to their internal ids
private val languageMappings = listOf(
Pair("japanese", listOf("0", "1024", "2048")),
Pair("english", listOf("1", "1025", "2049")),
Pair("chinese", listOf("10", "1034", "2058")),
Pair("dutch", listOf("20", "1044", "2068")),
Pair("french", listOf("30", "1054", "2078")),
Pair("german", listOf("40", "1064", "2088")),
Pair("hungarian", listOf("50", "1074", "2098")),
Pair("italian", listOf("60", "1084", "2108")),
Pair("korean", listOf("70", "1094", "2118")),
Pair("polish", listOf("80", "1104", "2128")),
Pair("portuguese", listOf("90", "1114", "2138")),
Pair("russian", listOf("100", "1124", "2148")),
Pair("spanish", listOf("110", "1134", "2158")),
Pair("thai", listOf("120", "1144", "2168")),
Pair("vietnamese", listOf("130", "1154", "2178")),
Pair("n/a", listOf("254", "1278", "2302")),
Pair("other", listOf("255", "1279", "2303"))
)
companion object {
const val QUERY_PREFIX = "?f_apply=Apply+Filter"
const val PREFIX_ID_SEARCH = "id:"
const val TR_SUFFIX = "TR"
// Preferences vals
private const val ENFORCE_LANGUAGE_PREF_KEY = "ENFORCE_LANGUAGE"
private const val ENFORCE_LANGUAGE_PREF_TITLE = "Enforce Language"
private const val ENFORCE_LANGUAGE_PREF_SUMMARY = "If checked, forces browsing of manga matching a language tag"
private const val ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE = false
}
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val enforceLanguagePref = CheckBoxPreference(screen.context).apply {
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
title = ENFORCE_LANGUAGE_PREF_TITLE
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(enforceLanguagePref)
}
override fun setupPreferenceScreen(screen: LegacyPreferenceScreen) {
val enforceLanguagePref = LegacyCheckBoxPreference(screen.context).apply {
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
title = ENFORCE_LANGUAGE_PREF_TITLE
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(enforceLanguagePref)
}
private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
}

View File

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
/**
* Gallery metadata storage model
*/
class ExGalleryMetadata {
var url: String? = null
var thumbnailUrl: String? = null
var title: String? = null
var altTitle: String? = null
var genre: String? = null
var datePosted: Long? = null
var parent: String? = null
var visible: String? = null // Not a boolean
var language: String? = null
var translated: Boolean? = null
var size: Long? = null
var length: Int? = null
var favorites: Int? = null
var ratingCount: Int? = null
var averageRating: Double? = null
var uploader: String? = null
val tags: MutableMap<String, List<Tag>> = mutableMapOf()
companion object {
private fun splitGalleryUrl(url: String) = url.let {
// Only parse URL if is full URL
val pathSegments = if (it.startsWith("http"))
Uri.parse(it).pathSegments
else
it.split('/')
pathSegments.filterNot(String::isNullOrBlank)
}
private fun galleryId(url: String) = splitGalleryUrl(url)[1]
private fun galleryToken(url: String) = splitGalleryUrl(url)[2]
private fun normalizeUrl(id: String, token: String) = "/g/$id/$token/?nw=always"
fun normalizeUrl(url: String) = normalizeUrl(galleryId(url), galleryToken(url))
}
}

View File

@ -1,84 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import eu.kanade.tachiyomi.source.model.SManga
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private const val EH_ARTIST_NAMESPACE = "artist"
private const val EH_AUTHOR_NAMESPACE = "author"
private val ONGOING_SUFFIX = arrayOf(
"[ongoing]",
"(ongoing)",
"{ongoing}"
)
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
fun ExGalleryMetadata.copyTo(manga: SManga) {
url?.let { manga.url = it }
thumbnailUrl?.let { manga.thumbnail_url = it }
(title ?: altTitle)?.let { manga.title = it }
// Set artist (if we can find one)
tags[EH_ARTIST_NAMESPACE]?.let {
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
}
// Set author (if we can find one)
tags[EH_AUTHOR_NAMESPACE]?.let {
if (it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
}
// Set genre
genre?.let { manga.genre = it }
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
// We default to completed
manga.status = SManga.COMPLETED
title?.let { t ->
if (ONGOING_SUFFIX.any {
t.endsWith(it, ignoreCase = true)
}
) manga.status = SManga.ONGOING
}
// Build a nice looking description out of what we know
val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" }
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
val detailsDesc = StringBuilder()
uploader?.let { detailsDesc += "Uploader: $it\n" }
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
visible?.let { detailsDesc += "Visible: $it\n" }
language?.let {
detailsDesc += "Language: $it"
if (translated == true) detailsDesc += " TR"
detailsDesc += "\n"
}
size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
length?.let { detailsDesc += "Length: $it pages\n" }
favorites?.let { detailsDesc += "Favorited: $it times\n" }
averageRating?.let {
detailsDesc += "Rating: $it"
ratingCount?.let { count -> detailsDesc += " ($count)" }
detailsDesc += "\n"
}
val tagsDesc = buildTagsDescription(this)
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")
}
private fun buildTagsDescription(metadata: ExGalleryMetadata) = StringBuilder("Tags:\n").apply {
// BiConsumer only available in Java 8, we have to use destructuring here
metadata.tags.forEach { (namespace, tags) ->
if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
this += "$namespace: $joinedTags\n"
}
}
}

View File

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
/**
* Simple tag model
*/
data class Tag(val name: String, val light: Boolean)

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
/**
* Uri filter
*/
interface UriFilter {
fun addToUri(builder: Uri.Builder)
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
import eu.kanade.tachiyomi.source.model.Filter
/**
* UriGroup
*/
open class UriGroup<V>(name: String, state: List<V>) : Filter.Group<V>(name, state), UriFilter {
override fun addToUri(builder: Uri.Builder) {
state.forEach {
if (it is UriFilter) it.addToUri(builder)
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'HentaiHand'
pkgNameSuffix = 'all.hentaihand'
extClass = '.HentaiHandFactory'
extVersionCode = 2
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,396 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaihand
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.EditTextPreference
import android.support.v7.preference.PreferenceScreen
import android.text.InputType
import android.widget.Toast
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
@Nsfw
class HentaiHand(
override val lang: String,
private val hhLangId: Int? = null,
extraName: String = ""
) : ConfigurableSource, HttpSource() {
override val baseUrl: String = "https://hentaihand.com"
override val name: String = "HentaiHand$extraName"
override val supportsLatest = true
private val gson = Gson()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { authIntercept(it) }
.build()
private fun parseGenericResponse(response: Response): MangasPage {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
return MangasPage(
data.getAsJsonArray("data").map {
SManga.create().apply {
url = "/en/comic/${it["slug"].asString}"
title = it["title"].asString
thumbnail_url = it["thumb_url"].asString
}
},
!data["next_page_url"].isJsonNull
)
}
// Popular
override fun popularMangaParse(response: Response): MangasPage = parseGenericResponse(response)
override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=popularity&order=desc&duration=all"
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
}
// Latest
override fun latestUpdatesParse(response: Response): MangasPage = parseGenericResponse(response)
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/api/comics?page=$page&sort=uploaded_at&order=desc&duration=week"
return GET(if (hhLangId == null) url else ("$url&languages=$hhLangId"))
}
// Search
override fun searchMangaParse(response: Response): MangasPage = parseGenericResponse(response)
private fun lookupFilterId(query: String, uri: String): Int? {
// filter query needs to be resolved to an ID
return client.newCall(GET("$baseUrl/api/$uri?q=$query"))
.asObservableSuccess()
.subscribeOn(Schedulers.io())
.map {
val data = gson.fromJson<JsonObject>(it.body()!!.string())
// only the first tag will be used
data.getAsJsonArray("data").firstOrNull()?.let { t -> t["id"].asInt }
}.toBlocking().first()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/api/comics")!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("q", query)
if (hhLangId != null)
url.addQueryParameter("languages", hhLangId.toString())
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("sort", getSortPairs()[filter.state].second)
is OrderFilter -> url.addQueryParameter("order", getOrderPairs()[filter.state].second)
is DurationFilter -> url.addQueryParameter("duration", getDurationPairs()[filter.state].second)
is AttributesGroupFilter -> filter.state.forEach {
if (it.state) url.addQueryParameter("attributes", it.value)
}
is LookupFilter -> {
filter.state.split(",").map { it.trim() }.filter { it.isNotBlank() }.map {
lookupFilterId(it, filter.uri) ?: throw Exception("No ${filter.singularName} \"$it\" was found")
}.forEach {
if (!(filter.uri == "languages" && it == hhLangId))
url.addQueryParameter(filter.uri, it.toString())
}
}
else -> {}
}
}
return GET(url.toString())
}
// Details
private fun tagArrayToString(array: JsonArray, key: String = "name"): String? {
if (array.size() == 0)
return null
return array.joinToString { it[key].asString }
}
private fun mangaDetailsApiRequest(manga: SManga): Request {
val slug = manga.url.removePrefix("/en/comic/")
return GET("$baseUrl/api/comics/$slug")
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsApiRequest(manga))
.asObservableSuccess()
.map { mangaDetailsParse(it).apply { initialized = true } }
}
override fun mangaDetailsParse(response: Response): SManga {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
return SManga.create().apply {
artist = tagArrayToString(data.getAsJsonArray("artists"))
author = tagArrayToString(data.getAsJsonArray("authors")) ?: artist
genre = listOf("tags", "relationships").map {
data.getAsJsonArray(it).map { t -> t["name"].asString }
}.flatten().distinct().joinToString()
status = SManga.COMPLETED
description = listOf(
Pair("Alternative Title", data["alternative_title"].nullString),
Pair("Groups", tagArrayToString(data.getAsJsonArray("groups"))),
Pair("Description", data["description"].nullString),
Pair("Pages", data["pages"].asInt.toString()),
Pair("Category", data["category"].nullObj?.get("name")?.asString),
Pair("Language", data["language"].nullObj?.get("name")?.asString),
Pair("Parodies", tagArrayToString(data.getAsJsonArray("parodies"))),
Pair("Characters", tagArrayToString(data.getAsJsonArray("characters")))
).filter { !it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}:\n${it.second}" }
}
}
// Chapters
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
return listOf(
SChapter.create().apply {
url = "/en/comic/${data["slug"].asString}/reader/1"
name = "Chapter"
date_upload = DATE_FORMAT.parse(data["uploaded_at"].asString)?.time ?: 0
chapter_number = 1f
}
)
}
// Pages
override fun pageListRequest(chapter: SChapter): Request {
val slug = chapter.url.removePrefix("/en/comic/").removeSuffix("/reader/1")
return GET("$baseUrl/api/comics/$slug/images")
}
override fun pageListParse(response: Response): List<Page> {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
return data.getAsJsonArray("images").mapIndexed { i, it ->
Page(i, "/en/comic/${data["comic"]["slug"].asString}/reader/${it["page"].asInt}", it["source_url"].asString)
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
// Authorization
private fun authIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (username.isEmpty() or password.isEmpty()) {
return chain.proceed(request)
}
if (token.isEmpty()) {
token = this.login(chain, username, password)
}
val authRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authRequest)
}
private fun login(chain: Interceptor.Chain, username: String, password: String): String {
val jsonObject = JSONObject().apply {
this.put("username", username)
this.put("password", password)
this.put("remember_me", true)
}
val body = RequestBody.create(MEDIA_TYPE, jsonObject.toString())
val response = chain.proceed(POST("$baseUrl/api/login", headers, body))
if (response.code() == 401) {
throw Exception("Failed to login, check if username and password are correct")
}
if (response.body() == null)
throw Exception("Login response body is empty")
try {
return JSONObject(response.body()!!.string())
.getJSONObject("auth")
.getString("access_token")
} catch (e: JSONException) {
throw Exception("Cannot parse login response body")
}
}
private var token: String = ""
private val username by lazy { getPrefUsername() }
private val password by lazy { getPrefPassword() }
// Preferences
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true))
}
private fun androidx.preference.PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference {
return androidx.preference.EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
if (isPassword) {
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(screen.supportEditTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.supportEditTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password))
}
private fun PreferenceScreen.supportEditTextPreference(title: String, default: String, value: String): EditTextPreference {
return EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
private fun getPrefUsername(): String = preferences.getString(USERNAME_TITLE, USERNAME_DEFAULT)!!
private fun getPrefPassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
// Filters
private class SortFilter(sortPairs: List<Pair<String, String>>) : Filter.Select<String>("Sort By", sortPairs.map { it.first }.toTypedArray())
private class OrderFilter(orderPairs: List<Pair<String, String>>) : Filter.Select<String>("Order By", orderPairs.map { it.first }.toTypedArray())
private class DurationFilter(durationPairs: List<Pair<String, String>>) : Filter.Select<String>("Duration", durationPairs.map { it.first }.toTypedArray())
private class AttributeFilter(name: String, val value: String) : Filter.CheckBox(name)
private class AttributesGroupFilter(attributePairs: List<Pair<String, String>>) : Filter.Group<AttributeFilter>("Attributes", attributePairs.map { AttributeFilter(it.first, it.second) })
private class CategoriesFilter : LookupFilter("Categories", "categories", "category")
private class TagsFilter : LookupFilter("Tags", "tags", "tag")
private class ArtistsFilter : LookupFilter("Artists", "artists", "artist")
private class GroupsFilter : LookupFilter("Groups", "groups", "group")
private class CharactersFilter : LookupFilter("Characters", "characters", "character")
private class ParodiesFilter : LookupFilter("Parodies", "parodies", "parody")
private class LanguagesFilter : LookupFilter("Other Languages", "languages", "language")
open class LookupFilter(name: String, val uri: String, val singularName: String) : Filter.Text(name)
override fun getFilterList() = FilterList(
SortFilter(getSortPairs()),
OrderFilter(getOrderPairs()),
DurationFilter(getDurationPairs()),
Filter.Header("Separate terms with commas (,)"),
CategoriesFilter(),
TagsFilter(),
ArtistsFilter(),
GroupsFilter(),
CharactersFilter(),
ParodiesFilter(),
LanguagesFilter(),
AttributesGroupFilter(getAttributePairs())
)
private fun getSortPairs() = listOf(
Pair("Upload Date", "uploaded_at"),
Pair("Title", "title"),
Pair("Pages", "pages"),
Pair("Favorites", "favorites"),
Pair("Popularity", "popularity")
)
private fun getOrderPairs() = listOf(
Pair("Descending", "desc"),
Pair("Ascending", "asc")
)
private fun getDurationPairs() = listOf(
Pair("Today", "day"),
Pair("This Week", "week"),
Pair("This Month", "month"),
Pair("This Year", "year"),
Pair("All Time", "all")
)
private fun getAttributePairs() = listOf(
Pair("Translated", "translated"),
Pair("Speechless", "speechless"),
Pair("Rewritten", "rewritten")
)
companion object {
@SuppressLint("SimpleDateFormat")
private val DATE_FORMAT = SimpleDateFormat("yyyy-dd-MM")
private val MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8")
private const val USERNAME_TITLE = "Username"
private const val USERNAME_DEFAULT = ""
private const val PASSWORD_TITLE = "Password"
private const val PASSWORD_DEFAULT = ""
}
}

View File

@ -1,51 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaihand
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class HentaiHandFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
// https://hentaihand.com/api/languages?per_page=50
HentaiHand("other", extraName = " (Unfiltered)"),
HentaiHand("en", 1),
HentaiHand("zh", 2),
HentaiHand("ja", 3),
HentaiHand("other", 4, extraName = " (Text Cleaned)"),
HentaiHand("eo", 5),
HentaiHand("ceb", 6),
HentaiHand("cs", 7),
HentaiHand("ar", 8),
HentaiHand("sk", 9),
HentaiHand("mn", 10),
HentaiHand("uk", 11),
HentaiHand("la", 12),
HentaiHand("tl", 13),
HentaiHand("es", 14),
HentaiHand("it", 15),
HentaiHand("ko", 16),
HentaiHand("th", 17),
HentaiHand("pl", 18),
HentaiHand("fr", 19),
HentaiHand("pt", 20),
HentaiHand("de", 21),
HentaiHand("fi", 22),
HentaiHand("ru", 23),
HentaiHand("sv", 24),
HentaiHand("hu", 25),
HentaiHand("id", 26),
HentaiHand("vi", 27),
HentaiHand("da", 28),
HentaiHand("ro", 29),
HentaiHand("et", 30),
HentaiHand("nl", 31),
HentaiHand("ca", 32),
HentaiHand("tr", 33),
HentaiHand("el", 34),
HentaiHand("no", 35),
HentaiHand("sq", 1501),
HentaiHand("bg", 1502),
)
}

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.hitomi.HitomiActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPattern="/cg/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Hitomi.la'
pkgNameSuffix = 'all.hitomi'
extClass = '.HitomiFactory'
extVersionCode = 5
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,93 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import java.nio.ByteBuffer
/**
* Simple cursor for use on byte arrays
* @author nulldev
*/
class ByteCursor(val content: ByteArray) {
var index = -1
private set
private var mark = -1
fun mark() {
mark = index
}
fun jumpToMark() {
index = mark
}
fun jumpToIndex(index: Int) {
this.index = index
}
fun next(): Byte {
return content[++index]
}
fun next(count: Int): ByteArray {
val res = content.sliceArray(index + 1..index + count)
skip(count)
return res
}
// Used to perform conversions
private fun byteBuffer(count: Int): ByteBuffer {
return ByteBuffer.wrap(next(count))
}
// Epic hack to get an unsigned short properly...
fun fakeNextShortInt(): Int = ByteBuffer
.wrap(arrayOf(0x00, 0x00, *next(2).toTypedArray()).toByteArray())
.getInt(0)
// fun nextShort(): Short = byteBuffer(2).getShort(0)
fun nextInt(): Int = byteBuffer(4).getInt(0)
fun nextLong(): Long = byteBuffer(8).getLong(0)
fun nextFloat(): Float = byteBuffer(4).getFloat(0)
fun nextDouble(): Double = byteBuffer(8).getDouble(0)
fun skip(count: Int) {
index += count
}
fun expect(vararg bytes: Byte) {
if (bytes.size > remaining()) {
throw IllegalStateException("Unexpected end of content!")
}
for (i in 0..bytes.lastIndex) {
val expected = bytes[i]
val actual = content[index + i + 1]
if (expected != actual) {
throw IllegalStateException("Unexpected content (expected: $expected, actual: $actual)!")
}
}
index += bytes.size
}
fun checkEqual(vararg bytes: Byte): Boolean {
if (bytes.size > remaining()) {
return false
}
for (i in 0..bytes.lastIndex) {
val expected = bytes[i]
val actual = content[index + i + 1]
if (expected != actual) {
return false
}
}
return true
}
fun atEnd() = index >= content.size - 1
fun remaining() = content.size - index - 1
}

View File

@ -1,406 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.CheckBoxPreference
import android.support.v7.preference.PreferenceScreen
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.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import androidx.preference.CheckBoxPreference as AndroidXCheckBoxPreference
import androidx.preference.PreferenceScreen as AndroidXPreferenceScreen
/**
* Ported from TachiyomiSy
* Original work by NerdNumber9 for TachiyomiEH
*/
open class Hitomi(override val lang: String, private val nozomiLang: String) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val name = if (nozomiLang == "all") "Hitomi.la unfiltered" else "Hitomi.la"
override val baseUrl = BASE_URL
// Popular
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.flatMap { responseToMangas(it) }
}
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/popular-$nozomiLang.nozomi",
100L * (page - 1),
99L + 100 * (page - 1)
)
private fun responseToMangas(response: Response): Observable<MangasPage> {
val range = response.header("Content-Range")!!
val total = range.substringAfter('/').toLong()
val end = range.substringBefore('/').substringAfter('-').toLong()
val body = response.body()!!
return parseNozomiPage(body.bytes())
.map {
MangasPage(it, end < total - 1)
}
}
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
val cursor = ByteCursor(array)
val ids = (1..array.size / 4).map {
cursor.nextInt()
}
return nozomiIdsToMangas(ids).toObservable()
}
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
return Single.zip(
ids.map { int ->
client.newCall(GET("$LTN_BASE_URL/galleryblock/$int.html"))
.asObservableSuccess()
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
.map { parseGalleryBlock(it) }
.toSingle()
}
) { it.map { m -> m as SManga } }
}
private fun Document.selectFirst(selector: String) = this.select(selector).first()
private fun parseGalleryBlock(response: Response): SManga {
val doc = response.asJsoup()
return SManga.create().apply {
val titleElement = doc.selectFirst("h1")
title = titleElement.text()
thumbnail_url = "https:" + if (useHqThumbPref()) {
doc.selectFirst("img").attr("srcset").substringBefore(' ')
} else {
doc.selectFirst("img").attr("src")
}
url = titleElement.child(0).attr("href")
}
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException("Not used")
// Latest
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.flatMap { responseToMangas(it) }
}
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/index-$nozomiLang.nozomi",
100L * (page - 1),
99L + 100 * (page - 1)
)
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not used")
// Search
private var cachedTagIndexVersion: Long? = null
private var tagIndexVersionCacheTime: Long = 0
private fun tagIndexVersion(): Single<Long> {
val sCachedTagIndexVersion = cachedTagIndexVersion
return if (sCachedTagIndexVersion == null ||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedTagIndexVersion)
}
}
private var cachedGalleryIndexVersion: Long? = null
private var galleryIndexVersionCacheTime: Long = 0
private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null ||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it
galleryIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedGalleryIndexVersion)
}
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(GET("$baseUrl/cg/$id", headers)).asObservableSuccess()
.map { MangasPage(listOf(mangaDetailsParse(it).apply { url = "/cg/$id" }), false) }
} else {
val splitQuery = query.toLowerCase(Locale.ENGLISH).split(" ")
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
if (nozomiLang != "all") positive += "language:$nozomiLang"
val negative = (splitQuery - positive).map { it.removePrefix("-") }
// TODO Cache the results coming out of HitomiNozomi (this TODO dates back to TachiyomiEH)
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
.map { HitomiNozomi(client, it.first, it.second) }
var base = if (positive.isEmpty()) {
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
} else {
val q = positive.removeAt(0)
hn.flatMap { n -> n.getGalleryIdsForQuery(q).map { n to it.toSet() } }
}
base = positive.fold(base) { acc, q ->
acc.flatMap { (nozomi, mangas) ->
nozomi.getGalleryIdsForQuery(q).map {
nozomi to mangas.intersect(it)
}
}
}
base = negative.fold(base) { acc, q ->
acc.flatMap { (nozomi, mangas) ->
nozomi.getGalleryIdsForQuery(q).map {
nozomi to (mangas - it)
}
}
}
base.flatMap { (_, ids) ->
val chunks = ids.chunked(PAGE_SIZE)
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
MangasPage(mangas, page < chunks.size)
}
}.toObservable()
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used")
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not used")
// Details
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
fun String.replaceSpaces() = this.replace(" ", "_")
return SManga.create().apply {
thumbnail_url = document.select("div.cover img").attr("abs:src")
author = document.select("div.gallery h2 a").joinToString { it.text() }
val tableInfo = document.select("table tr")
.map { tr ->
val key = tr.select("td:first-child").text()
val value = with(tr.select("td:last-child a")) {
when (key) {
"Series", "Characters" -> {
if (text().isNotEmpty())
joinToString { "${attr("href").removePrefix("/").substringBefore("/")}:${it.text().replaceSpaces()}" } else null
}
"Tags" -> joinToString { element ->
element.text().let {
when {
it.contains("") -> "female:${it.substringBeforeLast(" ").replaceSpaces()}"
it.contains("") -> "male:${it.substringBeforeLast(" ").replaceSpaces()}"
else -> it
}
}
}
else -> joinToString { it.text() }
}
}
Pair(key, value)
}
.plus(Pair("Date uploaded", document.select("div.gallery span.date").text()))
.toMap()
description = tableInfo.filterNot { it.value.isNullOrEmpty() || it.key in listOf("Series", "Characters", "Tags") }.entries.joinToString("\n") { "${it.key}: ${it.value}" }
genre = listOfNotNull(tableInfo["Series"], tableInfo["Characters"], tableInfo["Tags"]).joinToString()
}
}
// Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 0.0f
}
)
)
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used")
// Pages
private fun hlIdFromUrl(url: String) =
url.split('/').last().split('-').last().substringBeforeLast('.')
override fun pageListRequest(chapter: SChapter): Request {
return GET("$LTN_BASE_URL/galleries/${hlIdFromUrl(chapter.url)}.js")
}
private val jsonParser = JsonParser()
override fun pageListParse(response: Response): List<Page> {
val str = response.body()!!.string()
val json = jsonParser.parse(str.removePrefix("var galleryinfo = "))
return json["files"].array.mapIndexed { i, jsonElement ->
val hash = jsonElement["hash"].string
val ext = if (jsonElement["haswebp"].string == "0" || !hitomiAlwaysWebp()) jsonElement["name"].string.split('.').last() else "webp"
val path = if (jsonElement["haswebp"].string == "0" || !hitomiAlwaysWebp()) "images" else "webp"
val hashPath1 = hash.takeLast(1)
val hashPath2 = hash.takeLast(3).take(2)
// https://ltn.hitomi.la/reader.js
// function make_image_element()
val secondSubdomain = if (jsonElement["haswebp"].string == "0" && jsonElement["hasavif"].string == "0") "b" else "a"
Page(i, "", "https://${firstSubdomainFromGalleryId(hashPath2)}$secondSubdomain.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext")
}
}
// https://ltn.hitomi.la/common.js
private fun firstSubdomainFromGalleryId(pathSegment: String): Char {
var numberOfFrontends = 3
var g = pathSegment.toInt(16)
if (g < 0x30) numberOfFrontends = 2
if (g < 0x09) g = 1
return (97 + g.rem(numberOfFrontends)).toChar()
}
override fun imageRequest(page: Page): Request {
val request = super.imageRequest(page)
val hlId = request.url().pathSegments().let {
it[it.lastIndex - 1]
}
return request.newBuilder()
.header("Referer", "$BASE_URL/reader/$hlId.html")
.build()
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used")
companion object {
private const val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
private const val PAGE_SIZE = 25
const val PREFIX_ID_SEARCH = "id:"
// From HitomiSearchMetaData
const val LTN_BASE_URL = "https://ltn.hitomi.la"
const val BASE_URL = "https://hitomi.la"
// Preferences
private const val WEBP_PREF_KEY = "HITOMI_WEBP"
private const val WEBP_PREF_TITLE = "Webp pages"
private const val WEBP_PREF_SUMMARY = "Download webp pages instead of jpeg (when available)"
private const val WEBP_PREF_DEFAULT_VALUE = true
private const val COVER_PREF_KEY = "HITOMI_COVERS"
private const val COVER_PREF_TITLE = "Use HQ covers"
private const val COVER_PREF_SUMMARY = "See HQ covers while browsing"
private const val COVER_PREF_DEFAULT_VALUE = true
}
// Preferences
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val webpPref = CheckBoxPreference(screen.context).apply {
key = "${WEBP_PREF_KEY}_$lang"
title = WEBP_PREF_TITLE
summary = WEBP_PREF_SUMMARY
setDefaultValue(WEBP_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${WEBP_PREF_KEY}_$lang", checkValue).commit()
}
}
val coverPref = CheckBoxPreference(screen.context).apply {
key = "${COVER_PREF_KEY}_$lang"
title = COVER_PREF_TITLE
summary = COVER_PREF_SUMMARY
setDefaultValue(COVER_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${COVER_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(webpPref)
screen.addPreference(coverPref)
}
override fun setupPreferenceScreen(screen: AndroidXPreferenceScreen) {
val webpPref = AndroidXCheckBoxPreference(screen.context).apply {
key = "${WEBP_PREF_KEY}_$lang"
title = WEBP_PREF_TITLE
summary = WEBP_PREF_SUMMARY
setDefaultValue(WEBP_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${WEBP_PREF_KEY}_$lang", checkValue).commit()
}
}
val coverPref = AndroidXCheckBoxPreference(screen.context).apply {
key = "${COVER_PREF_KEY}_$lang"
title = COVER_PREF_TITLE
summary = COVER_PREF_SUMMARY
setDefaultValue(COVER_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${COVER_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(webpPref)
screen.addPreference(coverPref)
}
private fun hitomiAlwaysWebp(): Boolean = preferences.getBoolean("${WEBP_PREF_KEY}_$lang", WEBP_PREF_DEFAULT_VALUE)
private fun useHqThumbPref(): Boolean = preferences.getBoolean("${COVER_PREF_KEY}_$lang", COVER_PREF_DEFAULT_VALUE)
}

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://hitomi.la/cg/xxxx intents
* and redirects them to the main Tachiyomi process.
*/
class HitomiActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Hitomi.PREFIX_ID_SEARCH}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("HitomiActivity", e.toString())
}
} else {
Log.e("HitomiActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class HitomiFactory : SourceFactory {
override fun createSources(): List<Source> = languageList
.map { Hitomi(it.first, it.second) }
}
private val languageList = listOf(
Pair("other", "all"), // all languages
Pair("id", "indonesian"),
Pair("ca", "catalan"),
Pair("ceb", "cebuano"),
Pair("cs", "czech"),
Pair("da", "danish"),
Pair("de", "german"),
Pair("et", "estonian"),
Pair("en", "english"),
Pair("es", "spanish"),
Pair("eo", "esperanto"),
Pair("fr", "french"),
Pair("it", "italian"),
Pair("la", "latin"),
Pair("hu", "hungarian"),
Pair("nl", "dutch"),
Pair("no", "norwegian"),
Pair("pl", "polish"),
Pair("pt-BR", "portuguese"),
Pair("ro", "romanian"),
Pair("sq", "albanian"),
Pair("sk", "slovak"),
Pair("fi", "finnish"),
Pair("sv", "swedish"),
Pair("tl", "tagalog"),
Pair("vi", "vietnamese"),
Pair("tr", "turkish"),
Pair("el", "greek"),
Pair("mn", "mongolian"),
Pair("ru", "russian"),
Pair("uk", "ukrainian"),
Pair("he", "hebrew"),
Pair("ar", "arabic"),
Pair("fa", "persian"),
Pair("th", "thai"),
Pair("ko", "korean"),
Pair("zh", "chinese"),
Pair("ja", "japanese")
)

View File

@ -1,257 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import eu.kanade.tachiyomi.extension.all.hitomi.Hitomi.Companion.LTN_BASE_URL
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
import rx.Single
import java.security.MessageDigest
private typealias HashedTerm = ByteArray
private data class DataPair(val offset: Long, val length: Int)
private data class Node(
val keys: List<ByteArray>,
val datas: List<DataPair>,
val subnodeAddresses: List<Long>
)
/**
* Kotlin port of the hitomi.la search algorithm
* @author NerdNumber9
*/
class HitomiNozomi(
private val client: OkHttpClient,
private val tagIndexVersion: Long,
private val galleriesIndexVersion: Long
) {
fun getGalleryIdsForQuery(query: String): Single<List<Int>> {
val replacedQuery = query.replace('_', ' ')
if (':' in replacedQuery) {
val sides = replacedQuery.split(':')
val namespace = sides[0]
var tag = sides[1]
var area: String? = namespace
var language = "all"
if (namespace == "female" || namespace == "male") {
area = "tag"
tag = replacedQuery
} else if (namespace == "language") {
area = null
language = tag
tag = "index"
}
return getGalleryIdsFromNozomi(area, tag, language)
}
val key = hashTerm(query)
val field = "galleries"
return getNodeAtAddress(field, 0).flatMap { node ->
if (node == null) {
Single.just(null)
} else {
BSearch(field, key, node).flatMap { data ->
if (data == null) {
Single.just(null)
} else {
getGalleryIdsFromData(data)
}
}
}
}
}
private fun getGalleryIdsFromData(data: DataPair?): Single<List<Int>> {
if (data == null) {
return Single.just(emptyList())
}
val url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.data"
val (offset, length) = data
if (length > 100000000 || length <= 0) {
return Single.just(emptyList())
}
return client.newCall(rangedGet(url, offset, offset + length - 1))
.asObservable()
.map {
it.body()?.bytes() ?: ByteArray(0)
}
.onErrorReturn { ByteArray(0) }
.map { inbuf ->
if (inbuf.isEmpty()) {
return@map emptyList<Int>()
}
val view = ByteCursor(inbuf)
val numberOfGalleryIds = view.nextInt()
val expectedLength = numberOfGalleryIds * 4 + 4
if (numberOfGalleryIds > 10000000 ||
numberOfGalleryIds <= 0 ||
inbuf.size != expectedLength
) {
return@map emptyList<Int>()
}
(1..numberOfGalleryIds).map {
view.nextInt()
}
}.toSingle()
}
@Suppress("FunctionName")
private fun BSearch(field: String, key: ByteArray, node: Node?): Single<DataPair?> {
fun compareByteArrays(dv1: ByteArray, dv2: ByteArray): Int {
val top = dv1.size.coerceAtMost(dv2.size)
for (i in 0 until top) {
val dv1i = dv1[i].toInt() and 0xFF
val dv2i = dv2[i].toInt() and 0xFF
if (dv1i < dv2i) {
return -1
} else if (dv1i > dv2i) {
return 1
}
}
return 0
}
fun locateKey(key: ByteArray, node: Node): Pair<Boolean, Int> {
var cmpResult = -1
var lastI = 0
for (nodeKey in node.keys) {
cmpResult = compareByteArrays(key, nodeKey)
if (cmpResult <= 0) break
lastI++
}
return (cmpResult == 0) to lastI
}
fun isLeaf(node: Node): Boolean {
return !node.subnodeAddresses.any {
it != 0L
}
}
if (node == null || node.keys.isEmpty()) {
return Single.just(null)
}
val (there, where) = locateKey(key, node)
if (there) {
return Single.just(node.datas[where])
} else if (isLeaf(node)) {
return Single.just(null)
}
return getNodeAtAddress(field, node.subnodeAddresses[where]).flatMap { newNode ->
BSearch(field, key, newNode)
}
}
private fun decodeNode(data: ByteArray): Node {
val view = ByteCursor(data)
val numberOfKeys = view.nextInt()
val keys = (1..numberOfKeys).map {
val keySize = view.nextInt()
view.next(keySize)
}
val numberOfDatas = view.nextInt()
val datas = (1..numberOfDatas).map {
val offset = view.nextLong()
val length = view.nextInt()
DataPair(offset, length)
}
val numberOfSubnodeAddresses = B + 1
val subnodeAddresses = (1..numberOfSubnodeAddresses).map {
view.nextLong()
}
return Node(keys, datas, subnodeAddresses)
}
private fun getNodeAtAddress(field: String, address: Long): Single<Node?> {
var url = "$LTN_BASE_URL/$INDEX_DIR/$field.$tagIndexVersion.index"
if (field == "galleries") {
url = "$LTN_BASE_URL/$GALLERIES_INDEX_DIR/galleries.$galleriesIndexVersion.index"
}
return client.newCall(rangedGet(url, address, address + MAX_NODE_SIZE - 1))
.asObservableSuccess()
.map {
it.body()?.bytes() ?: ByteArray(0)
}
.onErrorReturn { ByteArray(0) }
.map { nodedata ->
if (nodedata.isNotEmpty()) {
decodeNode(nodedata)
} else null
}.toSingle()
}
fun getGalleryIdsFromNozomi(area: String?, tag: String, language: String): Single<List<Int>> {
var nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$tag-$language$NOZOMI_EXTENSION"
if (area != null) {
nozomiAddress = "$LTN_BASE_URL/$COMPRESSED_NOZOMI_PREFIX/$area/$tag-$language$NOZOMI_EXTENSION"
}
return client.newCall(
Request.Builder()
.url(nozomiAddress)
.build()
)
.asObservableSuccess()
.map { resp ->
val body = resp.body()!!.bytes()
val cursor = ByteCursor(body)
(1..body.size / 4).map {
cursor.nextInt()
}
}.toSingle()
}
private fun hashTerm(query: String): HashedTerm {
val md = MessageDigest.getInstance("SHA-256")
md.update(query.toByteArray(HASH_CHARSET))
return md.digest().copyOf(4)
}
companion object {
private const val INDEX_DIR = "tagindex"
private const val GALLERIES_INDEX_DIR = "galleriesindex"
private const val COMPRESSED_NOZOMI_PREFIX = "n"
private const val NOZOMI_EXTENSION = ".nozomi"
private const val MAX_NODE_SIZE = 464
private const val B = 16
private val HASH_CHARSET = Charsets.UTF_8
fun rangedGet(url: String, rangeBegin: Long, rangeEnd: Long?): Request {
return GET(
url,
Headers.Builder()
.add("Range", "bytes=$rangeBegin-${rangeEnd ?: ""}")
.build()
)
}
fun getIndexVersion(httpClient: OkHttpClient, name: String): Observable<Long> {
return httpClient.newCall(GET("$LTN_BASE_URL/$name/version?_=${System.currentTimeMillis()}"))
.asObservableSuccess()
.map { it.body()!!.string().toLong() }
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'IMHentai'
pkgNameSuffix = 'all.imhentai'
extClass = '.IMHentaiFactory'
extVersionCode = 2
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

View File

@ -1,267 +0,0 @@
package eu.kanade.tachiyomi.extension.all.imhentai
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import rx.Observable
class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
private val pageLoadHeaders: Headers = Headers.Builder().apply {
add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
add("X-Requested-With", "XMLHttpRequest")
}.build()
override val baseUrl: String = "https://imhentai.xxx"
override val name: String = "IMHentai"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// Popular
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
thumbnail_url = element.select(".inner_thumb img").attr("src")
with(element.select(".caption a")) {
url = this.attr("href")
title = this.text()
}
}
}
override fun popularMangaNextPageSelector(): String = ".pagination li a:contains(Next):not([tabindex])"
override fun popularMangaSelector(): String = ".thumbs_container .thumb"
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_POPULAR))
// Latest
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_LATEST))
override fun latestUpdatesSelector(): String = popularMangaSelector()
// Search
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
private fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/search")!!.newBuilder()
.addQueryParameter("key", query)
.addQueryParameter("page", page.toString())
.addQueryParameter(getLanguageURIByName(imhLang).uri, toBinary(true)) // main language always enabled
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is LanguageFilters -> {
filter.state.forEach {
url.addQueryParameter(it.uri, toBinary(it.state))
}
}
is CategoryFilters -> {
filter.state.forEach {
url.addQueryParameter(it.uri, toBinary(it.state))
}
}
is SortOrderFilter -> {
getSortOrderURIs().forEachIndexed { index, pair ->
url.addQueryParameter(pair.second, toBinary(filter.state == index))
}
}
else -> { }
}
}
return GET(url.toString())
}
override fun searchMangaSelector(): String = popularMangaSelector()
// Details
private fun Elements.csvText(splitTagSeparator: String = ", "): String {
return this.joinToString {
listOf(
it.ownText(),
it.select(".split_tag")?.text()
?.trim()
?.removePrefix("| ")
)
.filter { s -> !s.isNullOrBlank() }
.joinToString(splitTagSeparator)
}
}
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val mangaInfoElement = document.select(".galleries_info")
val infoMap = mangaInfoElement.select("li:not(.pages)").map {
it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag")
}.toMap()
artist = infoMap["Artists"]?.csvText(" | ")
author = artist
genre = infoMap["Tags"]?.csvText()
status = SManga.COMPLETED
val pages = mangaInfoElement.select("li.pages").text().substringAfter("Pages: ")
val altTitle = document.select(".subtitle").text().ifBlank { null }
description = listOf(
"Parodies",
"Characters",
"Groups",
"Languages",
"Category"
).map { it to infoMap[it]?.csvText() }
.let { listOf(Pair("Alternate Title", altTitle)) + it + listOf(Pair("Pages", pages)) }
.filter { !it.second.isNullOrEmpty() }
.joinToString("\n\n") { "${it.first}:\n${it.second}" }
}
// Chapters
private fun pageLoadMetaParse(document: Document): String {
return document.select(".gallery_divider ~ input[type=\"hidden\"]").map { m ->
m.attr("id") to m.attr("value")
}.toMap().let {
listOf(
Pair("server", "load_server"),
Pair("u_id", "gallery_id"),
Pair("g_id", "load_id"),
Pair("img_dir", "load_dir"),
Pair("total_pages", "load_pages")
).map { meta -> "${meta.first}=${it[meta.second]}" }
.let { payload -> payload + listOf("type=2", "visible_pages=0") }
.joinToString("&")
}
}
override fun chapterListParse(response: Response): List<SChapter> {
return listOf(
SChapter.create().apply {
setUrlWithoutDomain(response.request().url().toString())
name = "Chapter"
chapter_number = 1f
}
)
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used")
override fun chapterListSelector(): String = throw UnsupportedOperationException("Not used")
// Pages
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(GET("$baseUrl${chapter.url}"))
.asObservableSuccess()
.map { pageLoadMetaParse(it.asJsoup()) }
.map { RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"), it) }
.concatMap { client.newCall(POST(PAEG_LOAD_URL, pageLoadHeaders, it)).asObservableSuccess() }
.map { pageListParse(it) }
}
override fun pageListParse(document: Document): List<Page> {
return document.select("a").mapIndexed { i, element ->
Page(i, element.attr("href"), element.select(".lazy.preloader[src]").attr("src").replace("t.", "."))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters
private class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>, state: Int) :
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state)
private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false)
private class LanguageFilters(flags: List<LanguageFilter>) : Filter.Group<LanguageFilter>("Other Languages", flags)
private class CategoryFilters(flags: List<SearchFlagFilter>) : Filter.Group<SearchFlagFilter>("Categories", flags)
override fun getFilterList() = getFilterList(SORT_ORDER_DEFAULT)
private fun getFilterList(sortOrderState: Int) = FilterList(
SortOrderFilter(getSortOrderURIs(), sortOrderState),
CategoryFilters(getCategoryURIs()),
LanguageFilters(getLanguageURIs().filter { it.name != imhLang }) // exclude main lang
)
private fun getCategoryURIs() = listOf(
SearchFlagFilter("Manga", "manga"),
SearchFlagFilter("Doujinshi", "doujinshi"),
SearchFlagFilter("Western", "western"),
SearchFlagFilter("Image Set", "imageset"),
SearchFlagFilter("Artist CG", "artistcg"),
SearchFlagFilter("Game CG", "gamecg")
)
// update sort order indices in companion object if order is changed
private fun getSortOrderURIs() = listOf(
Pair("Popular", "pp"),
Pair("Latest", "lt"),
Pair("Downloads", "dl"),
Pair("Top Rated", "tr")
)
private fun getLanguageURIs() = listOf(
LanguageFilter(LANGUAGE_ENGLISH, "en"),
LanguageFilter(LANGUAGE_JAPANESE, "jp"),
LanguageFilter(LANGUAGE_SPANISH, "es"),
LanguageFilter(LANGUAGE_FRENCH, "fr"),
LanguageFilter(LANGUAGE_KOREAN, "kr"),
LanguageFilter(LANGUAGE_GERMAN, "de"),
LanguageFilter(LANGUAGE_RUSSIAN, "ru")
)
private fun getLanguageURIByName(name: String): LanguageFilter {
return getLanguageURIs().first { it.name == name }
}
companion object {
// references to sort order indices
private const val SORT_ORDER_POPULAR = 0
private const val SORT_ORDER_LATEST = 1
private const val SORT_ORDER_DEFAULT = SORT_ORDER_POPULAR
// references to be used in factory
const val LANGUAGE_ENGLISH = "English"
const val LANGUAGE_JAPANESE = "Japanese"
const val LANGUAGE_SPANISH = "Spanish"
const val LANGUAGE_FRENCH = "French"
const val LANGUAGE_KOREAN = "Korean"
const val LANGUAGE_GERMAN = "German"
const val LANGUAGE_RUSSIAN = "Russian"
private const val PAEG_LOAD_URL: String = "https://imhentai.xxx/inc/thumbs_loader.php"
}
}

View File

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.extension.all.imhentai
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class IMHentaiFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
IMHentai("en", IMHentai.LANGUAGE_ENGLISH),
IMHentai("ja", IMHentai.LANGUAGE_JAPANESE),
IMHentai("es", IMHentai.LANGUAGE_SPANISH),
IMHentai("fr", IMHentai.LANGUAGE_FRENCH),
IMHentai("ko", IMHentai.LANGUAGE_KOREAN),
IMHentai("de", IMHentai.LANGUAGE_GERMAN),
IMHentai("ru", IMHentai.LANGUAGE_RUSSIAN)
)
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,175 +0,0 @@
## 1.2.23
Minimum Komga version required: `0.75.0`
### Features
* ignore DNS over HTTPS so it can reach IP addresses
## 1.2.22
Minimum Komga version required: `0.75.0`
### Features
* add error logs and better catch exceptions
## 1.2.21
Minimum Komga version required: `0.75.0`
### Features
* browse read lists (from the filter menu)
* filter by collection, respecting the collection's ordering
## 1.2.20
Minimum Komga version required: `0.75.0`
### Features
* filter by authors, grouped by role
## 1.2.19
Minimum Komga version required: `0.68.0`
### Features
* display Series authors
* display Series summary from books if no summary exists for Series
## 1.2.18
Minimum Komga version required: `0.63.2`
### Fix
* use metadata.releaseDate or fileLastModified for chapter date
## 1.2.17
Minimum Komga version required: `0.63.2`
### Fix
* list of collections for filtering could be empty in some conditions
## 1.2.16
Minimum Komga version required: `0.59.0`
### Features
* filter by genres, tags and publishers
## 1.2.15
Minimum Komga version required: `0.56.0`
### Features
* remove the 1000 chapters limit
* display series description and tags (genres + tags)
## 1.2.14
Minimum Komga version required: `0.41.0`
### Features
* change chapter display name to use the display number instead of the sort number
## 1.2.13
Minimum Komga version required: `0.41.0`
### Features
* compatibility for the upcoming version of Komga which have changes in the API (IDs are String instead of Long)
## 1.2.12
Minimum Komga version required: `0.41.0`
### Features
* filter by collection
## 1.2.11
Minimum Komga version required: `0.35.2`
### Features
* Set password preferences inputTypes
## 1.2.10
Minimum Komga version required: `0.35.2`
### Features
* unread only filter (closes gotson/komga#180)
* prefix book titles with number (closes gotson/komga#169)
## 1.2.9
Minimum Komga version required: `0.22.0`
### Features
* use SourceFactory to have multiple Komga servers (3 for the moment)
## 1.2.8
Minimum Komga version required: `0.22.0`
### Features
* use book metadata title for chapter display name
* use book metadata sort number for chapter number
## 1.2.7
### Features
* use series metadata title for display name
* filter on series status
## 1.2.6
### Features
* Add support for AndroidX preferences
## 1.2.5
### Features
* add sort options in filter
## 1.2.4
### Features
* better handling of authentication
## 1.2.3
### Features
* filters by library
## 1.2.2
### Features
* request converted image from server if format is not supported
## 1.2.1
### Features
* first version

View File

@ -1,17 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Komga'
pkgNameSuffix = 'all.komga'
extClass = '.KomgaFactory'
extVersionCode = 23
libVersion = '1.2'
}
dependencies {
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.8'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

View File

@ -1,559 +0,0 @@
package eu.kanade.tachiyomi.extension.all.komga
import android.app.Application
import android.content.SharedPreferences
import android.support.v7.preference.EditTextPreference
import android.support.v7.preference.PreferenceScreen
import android.text.InputType
import android.util.Log
import android.widget.Toast
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.extension.all.komga.dto.AuthorDto
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
import eu.kanade.tachiyomi.extension.all.komga.dto.CollectionDto
import eu.kanade.tachiyomi.extension.all.komga.dto.LibraryDto
import eu.kanade.tachiyomi.extension.all.komga.dto.PageDto
import eu.kanade.tachiyomi.extension.all.komga.dto.PageWrapperDto
import eu.kanade.tachiyomi.extension.all.komga.dto.ReadListDto
import eu.kanade.tachiyomi.extension.all.komga.dto.SeriesDto
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Credentials
import okhttp3.Dns
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Single
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
open class Komga(suffix: String = "") : ConfigurableSource, HttpSource() {
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/api/v1/series?page=${page - 1}", headers)
override fun popularMangaParse(response: Response): MangasPage =
processSeriesPage(response)
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/api/v1/series/latest?page=${page - 1}", headers)
override fun latestUpdatesParse(response: Response): MangasPage =
processSeriesPage(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val collectionId = (filters.find { it is CollectionSelect } as? CollectionSelect)?.let {
it.values[it.state].id
}
val type = when {
collectionId != null -> "collections/$collectionId/series"
filters.find { it is TypeSelect }?.state == 1 -> "readlists"
else -> "series"
}
val url = HttpUrl.parse("$baseUrl/api/v1/$type?search=$query&page=${page - 1}")!!.newBuilder()
filters.forEach { filter ->
when (filter) {
is UnreadOnly -> {
if (filter.state) {
url.addQueryParameter("read_status", "UNREAD")
}
}
is LibraryGroup -> {
val libraryToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
libraryToInclude.add(content.id)
}
}
if (libraryToInclude.isNotEmpty()) {
url.addQueryParameter("library_id", libraryToInclude.joinToString(","))
}
}
is StatusGroup -> {
val statusToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
statusToInclude.add(content.name.toUpperCase(Locale.ROOT))
}
}
if (statusToInclude.isNotEmpty()) {
url.addQueryParameter("status", statusToInclude.joinToString(","))
}
}
is GenreGroup -> {
val genreToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
genreToInclude.add(content.name)
}
}
if (genreToInclude.isNotEmpty()) {
url.addQueryParameter("genre", genreToInclude.joinToString(","))
}
}
is TagGroup -> {
val tagToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
tagToInclude.add(content.name)
}
}
if (tagToInclude.isNotEmpty()) {
url.addQueryParameter("tag", tagToInclude.joinToString(","))
}
}
is PublisherGroup -> {
val publisherToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
publisherToInclude.add(content.name)
}
}
if (publisherToInclude.isNotEmpty()) {
url.addQueryParameter("publisher", publisherToInclude.joinToString(","))
}
}
is AuthorGroup -> {
val authorToInclude = mutableListOf<AuthorDto>()
filter.state.forEach { content ->
if (content.state) {
authorToInclude.add(content.author)
}
}
authorToInclude.forEach {
url.addQueryParameter("author", "${it.name},${it.role}")
}
}
is Filter.Sort -> {
var sortCriteria = when (filter.state?.index) {
0 -> "metadata.titleSort"
1 -> "createdDate"
2 -> "lastModifiedDate"
else -> ""
}
if (sortCriteria.isNotEmpty()) {
sortCriteria += "," + if (filter.state?.ascending!!) "asc" else "desc"
url.addQueryParameter("sort", sortCriteria)
}
}
}
}
return GET(url.toString(), headers)
}
override fun searchMangaParse(response: Response): MangasPage =
processSeriesPage(response)
override fun mangaDetailsRequest(manga: SManga): Request =
GET(baseUrl + manga.url, headers)
override fun mangaDetailsParse(response: Response): SManga =
if (response.fromReadList()) {
val readList = gson.fromJson<ReadListDto>(response.body()?.charStream()!!)
readList.toSManga()
} else {
val series = gson.fromJson<SeriesDto>(response.body()?.charStream()!!)
series.toSManga()
}
override fun chapterListRequest(manga: SManga): Request =
GET("$baseUrl${manga.url}/books?unpaged=true&media_status=READY", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val page = gson.fromJson<PageWrapperDto<BookDto>>(response.body()?.charStream()!!).content
val r = page.map { book ->
SChapter.create().apply {
chapter_number = book.metadata.numberSort
name = "${if (!response.fromReadList()) "${book.metadata.number} - " else ""}${book.metadata.title} (${book.size})"
url = "$baseUrl/api/v1/books/${book.id}"
date_upload = book.metadata.releaseDate?.let { parseDate(it) }
?: parseDateTime(book.fileLastModified)
}
}
return if (!response.fromReadList()) r.sortedByDescending { it.chapter_number } else r.reversed()
}
override fun pageListRequest(chapter: SChapter): Request =
GET("${chapter.url}/pages")
override fun pageListParse(response: Response): List<Page> {
val pages = gson.fromJson<List<PageDto>>(response.body()?.charStream()!!)
return pages.map {
val url = "${response.request().url()}/${it.number}" +
if (!supportedImageTypes.contains(it.mediaType)) {
"?convert=png"
} else {
""
}
Page(
index = it.number - 1,
imageUrl = url
)
}
}
private fun processSeriesPage(response: Response): MangasPage {
if (response.fromReadList()) {
with(gson.fromJson<PageWrapperDto<ReadListDto>>(response.body()?.charStream()!!)) {
return MangasPage(content.map { it.toSManga() }, !last)
}
} else {
with(gson.fromJson<PageWrapperDto<SeriesDto>>(response.body()?.charStream()!!)) {
return MangasPage(content.map { it.toSManga() }, !last)
}
}
}
private fun SeriesDto.toSManga(): SManga =
SManga.create().apply {
title = metadata.title
url = "/api/v1/series/$id"
thumbnail_url = "$baseUrl/api/v1/series/$id/thumbnail"
status = when (metadata.status) {
"ONGOING" -> SManga.ONGOING
"ENDED" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = (metadata.genres + metadata.tags).joinToString(", ")
description = metadata.summary.ifBlank { booksMetadata.summary }
booksMetadata.authors.groupBy { it.role }.let { map ->
author = map["writer"]?.map { it.name }?.distinct()?.joinToString()
artist = map["penciller"]?.map { it.name }?.distinct()?.joinToString()
}
}
private fun ReadListDto.toSManga(): SManga =
SManga.create().apply {
title = name
url = "/api/v1/readlists/$id"
thumbnail_url = "$baseUrl/api/v1/readlists/$id/thumbnail"
status = SManga.UNKNOWN
}
private fun Response.fromReadList() = request().url().toString().contains("/api/v1/readlists")
private fun parseDate(date: String?): Long =
if (date == null)
Date().time
else {
try {
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(date).time
} catch (ex: Exception) {
Date().time
}
}
private fun parseDateTime(date: String?): Long =
if (date == null)
Date().time
else {
try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).parse(date).time
} catch (ex: Exception) {
try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US).parse(date).time
} catch (ex: Exception) {
Date().time
}
}
}
override fun imageUrlParse(response: Response): String = ""
private class TypeSelect : Filter.Select<String>("Search for", arrayOf(TYPE_SERIES, TYPE_READLISTS))
private class LibraryFilter(val id: String, name: String) : Filter.CheckBox(name, false)
private class LibraryGroup(libraries: List<LibraryFilter>) : Filter.Group<LibraryFilter>("Libraries", libraries)
private class CollectionSelect(collections: List<CollectionFilterEntry>) : Filter.Select<CollectionFilterEntry>("Collection", collections.toTypedArray())
private class SeriesSort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date added", "Date updated"), Selection(0, true))
private class StatusFilter(name: String) : Filter.CheckBox(name, false)
private class StatusGroup(filters: List<StatusFilter>) : Filter.Group<StatusFilter>("Status", filters)
private class UnreadOnly : Filter.CheckBox("Unread only", false)
private class GenreFilter(genre: String) : Filter.CheckBox(genre, false)
private class GenreGroup(genres: List<GenreFilter>) : Filter.Group<GenreFilter>("Genres", genres)
private class TagFilter(tag: String) : Filter.CheckBox(tag, false)
private class TagGroup(tags: List<TagFilter>) : Filter.Group<TagFilter>("Tags", tags)
private class PublisherFilter(publisher: String) : Filter.CheckBox(publisher, false)
private class PublisherGroup(publishers: List<PublisherFilter>) : Filter.Group<PublisherFilter>("Publishers", publishers)
private class AuthorFilter(val author: AuthorDto) : Filter.CheckBox(author.name, false)
private class AuthorGroup(role: String, authors: List<AuthorFilter>) : Filter.Group<AuthorFilter>(role, authors)
private data class CollectionFilterEntry(
val name: String,
val id: String? = null
) {
override fun toString() = name
}
override fun getFilterList(): FilterList {
val filters = try {
mutableListOf<Filter<*>>(
UnreadOnly(),
TypeSelect(),
CollectionSelect(listOf(CollectionFilterEntry("None")) + collections.map { CollectionFilterEntry(it.name, it.id) }),
LibraryGroup(libraries.map { LibraryFilter(it.id, it.name) }.sortedBy { it.name.toLowerCase() }),
StatusGroup(listOf("Ongoing", "Ended", "Abandoned", "Hiatus").map { StatusFilter(it) }),
GenreGroup(genres.map { GenreFilter(it) }),
TagGroup(tags.map { TagFilter(it) }),
PublisherGroup(publishers.map { PublisherFilter(it) })
).also {
it.addAll(authors.map { (role, authors) -> AuthorGroup(role, authors.map { AuthorFilter(it) }) })
it.add(SeriesSort())
}
} catch (e: Exception) {
Log.e(LOG_TAG, "error while creating filter list", e)
emptyList()
}
return FilterList(filters)
}
private var libraries = emptyList<LibraryDto>()
private var collections = emptyList<CollectionDto>()
private var genres = emptySet<String>()
private var tags = emptySet<String>()
private var publishers = emptySet<String>()
private var authors = emptyMap<String, List<AuthorDto>>() // roles to list of authors
override val name = "Komga${if (suffix.isNotBlank()) " ($suffix)" else ""}"
override val lang = "en"
override val supportsLatest = true
private val LOG_TAG = "extension.all.komga${if (suffix.isNotBlank()) ".$suffix" else ""}"
override val baseUrl by lazy { getPrefBaseUrl() }
private val username by lazy { getPrefUsername() }
private val password by lazy { getPrefPassword() }
private val gson by lazy { Gson() }
override fun headersBuilder(): Headers.Builder =
Headers.Builder()
.add("User-Agent", "Tachiyomi Komga v${BuildConfig.VERSION_NAME}")
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client: OkHttpClient =
network.client.newBuilder()
.authenticator { _, response ->
if (response.request().header("Authorization") != null) {
null // Give up, we've already failed to authenticate.
} else {
response.request().newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
.build()
}
}
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build()
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
screen.addPreference(screen.editTextPreference(ADDRESS_TITLE, ADDRESS_DEFAULT, baseUrl))
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.editTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password, true))
}
private fun androidx.preference.PreferenceScreen.editTextPreference(title: String, default: String, value: String, isPassword: Boolean = false): androidx.preference.EditTextPreference {
return androidx.preference.EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
if (isPassword) {
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
}
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(screen.supportEditTextPreference(ADDRESS_TITLE, ADDRESS_DEFAULT, baseUrl))
screen.addPreference(screen.supportEditTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))
screen.addPreference(screen.supportEditTextPreference(PASSWORD_TITLE, PASSWORD_DEFAULT, password))
}
private fun PreferenceScreen.supportEditTextPreference(title: String, default: String, value: String): EditTextPreference {
return EditTextPreference(context).apply {
key = title
this.title = title
summary = value
this.setDefaultValue(default)
dialogTitle = title
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(title, newValue as String).commit()
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
private fun getPrefUsername(): String = preferences.getString(USERNAME_TITLE, USERNAME_DEFAULT)!!
private fun getPrefPassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
init {
if (baseUrl.isNotBlank()) {
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/libraries", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
libraries = try {
gson.fromJson(response.body()?.charStream()!!)
} catch (e: Exception) {
emptyList()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading libraries for filters", tr)
}
)
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/collections?unpaged=true", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
collections = try {
gson.fromJson<PageWrapperDto<CollectionDto>>(response.body()?.charStream()!!).content
} catch (e: Exception) {
emptyList()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading collections for filters", tr)
}
)
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/genres", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
genres = try {
gson.fromJson(response.body()?.charStream()!!)
} catch (e: Exception) {
emptySet()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading genres for filters", tr)
}
)
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/tags", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
tags = try {
gson.fromJson(response.body()?.charStream()!!)
} catch (e: Exception) {
emptySet()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading tags for filters", tr)
}
)
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/publishers", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
publishers = try {
gson.fromJson(response.body()?.charStream()!!)
} catch (e: Exception) {
emptySet()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading publishers for filters", tr)
}
)
Single.fromCallable {
client.newCall(GET("$baseUrl/api/v1/authors", headers)).execute()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response ->
authors = try {
val list: List<AuthorDto> = gson.fromJson(response.body()?.charStream()!!)
list.groupBy { it.role }
} catch (e: Exception) {
emptyMap()
}
},
{ tr ->
Log.e(LOG_TAG, "error while loading authors for filters", tr)
}
)
}
}
companion object {
private const val ADDRESS_TITLE = "Address"
private const val ADDRESS_DEFAULT = ""
private const val USERNAME_TITLE = "Username"
private const val USERNAME_DEFAULT = ""
private const val PASSWORD_TITLE = "Password"
private const val PASSWORD_DEFAULT = ""
private val supportedImageTypes = listOf("image/jpeg", "image/png", "image/gif", "image/webp")
private const val TYPE_SERIES = "Series"
private const val TYPE_READLISTS = "Read lists"
}
}

View File

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.extension.all.komga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class KomgaFactory : SourceFactory {
override fun createSources(): List<Source> =
listOf(
Komga(),
Komga("2"),
Komga("3")
)
}

View File

@ -1,115 +0,0 @@
package eu.kanade.tachiyomi.extension.all.komga.dto
data class LibraryDto(
val id: String,
val name: String
)
data class SeriesDto(
val id: String,
val libraryId: String,
val name: String,
val created: String?,
val lastModified: String?,
val fileLastModified: String,
val booksCount: Int,
val metadata: SeriesMetadataDto,
val booksMetadata: BookMetadataAggregationDto
)
data class SeriesMetadataDto(
val status: String,
val created: String?,
val lastModified: String?,
val title: String,
val titleSort: String,
val summary: String,
val summaryLock: Boolean,
val readingDirection: String,
val readingDirectionLock: Boolean,
val publisher: String,
val publisherLock: Boolean,
val ageRating: Int?,
val ageRatingLock: Boolean,
val language: String,
val languageLock: Boolean,
val genres: Set<String>,
val genresLock: Boolean,
val tags: Set<String>,
val tagsLock: Boolean
)
data class BookMetadataAggregationDto(
val authors: List<AuthorDto> = emptyList(),
val releaseDate: String?,
val summary: String,
val summaryNumber: String,
val created: String,
val lastModified: String
)
data class BookDto(
val id: String,
val seriesId: String,
val name: String,
val number: Float,
val created: String?,
val lastModified: String?,
val fileLastModified: String,
val sizeBytes: Long,
val size: String,
val media: MediaDto,
val metadata: BookMetadataDto
)
data class MediaDto(
val status: String,
val mediaType: String,
val pagesCount: Int
)
data class PageDto(
val number: Int,
val fileName: String,
val mediaType: String
)
data class BookMetadataDto(
val title: String,
val titleLock: Boolean,
val summary: String,
val summaryLock: Boolean,
val number: String,
val numberLock: Boolean,
val numberSort: Float,
val numberSortLock: Boolean,
val releaseDate: String?,
val releaseDateLock: Boolean,
val authors: List<AuthorDto>,
val authorsLock: Boolean
)
data class AuthorDto(
val name: String,
val role: String
)
data class CollectionDto(
val id: String,
val name: String,
val ordered: Boolean,
val seriesIds: List<String>,
val createdDate: String,
val lastModifiedDate: String,
val filtered: Boolean
)
data class ReadListDto(
val id: String,
val name: String,
val bookIds: List<String>,
val createdDate: String,
val lastModifiedDate: String,
val filtered: Boolean
)

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.extension.all.komga.dto
data class PageWrapperDto<T>(
val content: List<T>,
val empty: Boolean,
val first: Boolean,
val last: Boolean,
val number: Long,
val numberOfElements: Long,
val size: Long,
val totalElements: Long,
val totalPages: Long
)

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,17 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'LANraragi'
pkgNameSuffix = 'all.lanraragi'
extClass = '.LANraragi'
extVersionCode = 6
libVersion = '1.2'
}
dependencies {
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.6'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Some files were not shown because too many files have changed in this diff Show More