delegate tsumino

This commit is contained in:
Rani Sargees 2020-02-22 05:12:30 -05:00 committed by Jobobby04
parent df6cafd6d1
commit 58cce53746
7 changed files with 150 additions and 500 deletions

View File

@ -133,8 +133,10 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
}
"tags" -> {
tags += content.select("a").map {
val ns = if(it.attr("href").startsWith("/tag/male")) "male" else "female"
RaisedTag(ns, it.text().dropLast(2), TAG_TYPE_DEFAULT)
val ns = if(it.attr("href").startsWith("/tag/male")) "male"
else if (it.attr("href").startsWith("/tag/female")) "female"
else "misc"
RaisedTag(ns, it.text().dropLast(if (ns=="misc") 0 else 2), TAG_TYPE_DEFAULT)
}
}
}
@ -362,14 +364,16 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
val str = response.body!!.string()
val json = jsonParser.parse(str.removePrefix("var galleryinfo = "))
return json.array.mapIndexed { index, jsonElement ->
return json["files"].array.mapIndexed { index, jsonElement ->
val hash = jsonElement["hash"].string
val ext = if (jsonElement["haswebp"].string=="0") jsonElement["name"].string.split('.').last() else "webp"
val path = if (jsonElement["haswebp"].string=="0") "images" else "webp"
val hashPath1 = hash.takeLast(1)
val hashPath2 = hash.takeLast(3).take(2)
Page(
index,
"",
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/webp/$hashPath1/$hashPath2/$hash.webp"
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
)
}
}

View File

@ -1,66 +1,59 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import com.crashlytics.android.Crashlytics
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.toast
import exh.TSUMINO_SOURCE_ID
import exh.metadata.metadata.TsuminoSearchMetadata
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.BASE_URL
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.ui.captcha.ActionCompletionVerifier
import exh.ui.captcha.BrowserActionActivity
import exh.source.DelegatedHttpSource
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga
import okhttp3.*
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
class Tsumino(private val context: Context): ParsedHttpSource(),
LewdSource<TsuminoSearchMetadata, Document>,
ActionCompletionVerifier,
UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class
private val preferences: PreferencesHelper by injectLazy()
override val id = TSUMINO_SOURCE_ID
class Tsumino(delegate: HttpSource) : DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>, UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class;
override val lang = "en"
override val supportsLatest = true
override val name = "Tsumino"
override val baseUrl = BASE_URL
//Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters)
}
override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
if(lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry")
return null
return "https://tsumino.com/Book/Info/${uri.lastPathSegment}"
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
override fun parseIntoMetadata(metadata: TsuminoSearchMetadata, input: Document) {
with(metadata) {
tmId = TsuminoSearchMetadata.tmIdFromUrl(input.location()).toInt()
tags.clear()
// input.getElementsByClass(".book-page-image")?.first()?.attr("src")?.text()?.trim()?.let {
// thumbNail = it;
// }
input.getElementById("Title")?.text()?.let {
title = it.trim()
}
@ -123,389 +116,14 @@ class Tsumino(private val context: Context): ParsedHttpSource(),
}
}
}
fun genericMangaParse(response: Response): MangasPage {
val json = jsonParser.parse(response.body!!.string()!!).asJsonObject
val hasNextPage = json["pageNumber"].int < json["pageCount"].int
val manga = json["data"].array.map {
val obj = it.obj["entry"].obj
SManga.create().apply {
val id = obj["id"].long
url = TsuminoSearchMetadata.mangaUrlFromId(id.toString())
thumbnail_url = obj["thumbnailUrl"].asString
title = obj["title"].string
}
}
return MangasPage(manga, hasNextPage)
}
fun genericMangaRequest(page: Int,
query: String,
sort: SortType,
length: LengthType,
minRating: Int,
excludeParodies: Boolean = false,
advSearch: List<AdvSearchEntry> = emptyList())
= POST("$BASE_URL/Search/Operate", body = FormBody.Builder()
.add("PageNumber", page.toString())
.add("Text", query)
.add("Sort", sort.name)
.add("List", "0")
.add("Length", length.id.toString())
.add("MinimumRating", minRating.toString())
.apply {
advSearch.forEachIndexed { index, entry ->
add("Tags[$index][Type]", entry.type.toString())
add("Tags[$index][Text]", entry.text)
add("Tags[$index][Exclude]", entry.exclude.toString())
}
if(excludeParodies)
add("Exclude[]", "6")
}
.build())
enum class SortType {
Newest,
Oldest,
Alphabetical,
Rating,
Pages,
Views,
Random,
Comments,
Popularity
}
enum class LengthType(val id: Int) {
Any(0),
Short(1),
Medium(2),
Long(3)
}
override fun popularMangaSelector() = throw UnsupportedOperationException("Unused method called!")
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!")
override fun popularMangaRequest(page: Int) = genericMangaRequest(page,
"",
SortType.Random,
LengthType.Any,
0)
override fun popularMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException("Unused method called!")
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Unused method called!")
override fun latestUpdatesRequest(page: Int) = genericMangaRequest(page,
"",
SortType.Newest,
LengthType.Any,
0)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
//Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Append filters again, to provide fallback in case a filter is not provided
// Since we only work with the first filter when building the result, if the filter is provided,
// the original filter is ignored
val f = filters + getFilterList()
return genericMangaRequest(
page,
query,
SortType.values()[f.filterIsInstance<SortFilter>().first().state],
LengthType.values()[f.filterIsInstance<LengthFilter>().first().state],
f.filterIsInstance<MinimumRatingFilter>().first().state,
f.filterIsInstance<ExcludeParodiesFilter>().first().state,
f.filterIsInstance<AdvSearchEntryFilter>().flatMap { filter ->
val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank)
splitState.map {
AdvSearchEntry(filter.type, it.removePrefix("-"), it.startsWith("-"))
}
}
)
}
override fun searchMangaSelector() = throw UnsupportedOperationException("Unused method called!")
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Unused method called!")
override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga.apply {
initialized = true
}))
}
}
override fun mangaDetailsParse(document: Document)
= throw UnsupportedOperationException("Unused method called!")
/* override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div.book-page-container")
val manga = SManga.create()
manga.title = infoElement.select("#Title").text()
manga.artist = getArtists(document)
manga.author = manga.artist
manga.status = SManga.COMPLETED
manga.thumbnail_url = infoElement.select("img").attr("src")
manga.description = getDesc(document)
return manga
}
*/
override fun chapterListSelector() = throw UnsupportedOperationException("Unused method called!")
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Unused method called!")
override fun fetchChapterList(manga: SManga) = getOrLoadMetadata(manga.id) {
client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { it.asJsoup() }
.toSingle()
}.map {
listOf(
SChapter.create().apply {
url = "/entry/${it.tmId}"
name = "Chapter"
it.uploadDate?.let { date_upload = it }
chapter_number = 1f
}
)
}.toObservable()
override val client: OkHttpClient
// Do not call super here as we don't want auto-captcha detection here
get() = network.client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES)
.addNetworkInterceptor {
val cAspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()
var request = it.request()
if(cAspNetCookie.isNotBlank()) {
request = it.request()
.newBuilder()
.header("Cookie", "ASP.NET_SessionId=$cAspNetCookie")
.build()
}
val response = it.proceed(request)
val newCookie = response.headers("Set-Cookie").map(String::trim).find {
it.startsWith(ASP_NET_COOKIE_NAME)
}
if(newCookie != null) {
val res = newCookie.substringAfter('=')
.substringBefore(';')
.trim()
preferences.eh_ts_aspNetCookie().set(res)
}
response
}.build()
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val id = chapter.url.substringAfterLast('/')
val call = POST("$BASE_URL/Read/Load", body = FormBody.Builder().add("q", id).build())
return client.newCall(call).asObservableSuccess().map {
val page = client.newCall(GET("$BASE_URL/Read/Index/$id?page=1")).execute().asJsoup()
val numPages = page.select("h1").text().split(" ").last()
if (numPages.isNotEmpty()) {
val pageArr = Array(numPages.toInt()) {i -> (
page.select("#image-container").attr("data-cdn")
.replace("[PAGE]", (i+1).toString())
)}
val pageUrls = Array(numPages.toInt()) {i -> (
"$BASE_URL/Read/Index/$id?page="+(i+1).toString()
)}
pageUrls.mapIndexed {index, obj ->
Page(index, pageUrls[index], pageArr[index])
}
} else {
throw IOException("probably a captcha")
}
}.doOnError {
try {
val aspNetCookie = preferences.eh_ts_aspNetCookie().getOrDefault()
val cookiesMap = if (aspNetCookie.isNotBlank())
mapOf(ASP_NET_COOKIE_NAME to aspNetCookie)
else
emptyMap()
BrowserActionActivity.launchCaptcha(context,
this,
cookiesMap,
CAPTCHA_SCRIPT,
"$BASE_URL/Read/Auth/$id",
".book-read-button")
} catch(t: Throwable) {
Crashlytics.logException(t)
context.toast("Could not launch captcha-solving activity: ${t.message}")
}
}
}
override fun verifyComplete(url: String): Boolean {
return Uri.parse(url).pathSegments.getOrNull(1) == "View"
}
override fun pageListParse(document: Document) = throw UnsupportedOperationException("Unused method called!")
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Unused method called!")
data class AdvSearchEntry(val type: Int, val text: String, val exclude: Boolean)
override fun getFilterList() = FilterList(
Filter.Header("Separate tags with commas"),
Filter.Header("Prepend with dash to exclude"),
TagFilter(),
CategoryFilter(),
CollectionFilter(),
GroupFilter(),
ArtistFilter(),
ParodyFilter(),
CharactersFilter(),
UploaderFilter(),
Filter.Separator(),
SortFilter(),
LengthFilter(),
MinimumRatingFilter(),
ExcludeParodiesFilter()
)
class TagFilter : AdvSearchEntryFilter("Tags", 1)
class CategoryFilter : AdvSearchEntryFilter("Categories", 2)
class CollectionFilter : AdvSearchEntryFilter("Collections", 3)
class GroupFilter : AdvSearchEntryFilter("Groups", 4)
class ArtistFilter : AdvSearchEntryFilter("Artists", 5)
class ParodyFilter : AdvSearchEntryFilter("Parodies", 6)
class CharactersFilter : AdvSearchEntryFilter("Characters", 7)
class UploaderFilter : AdvSearchEntryFilter("Uploaders", 8)
open class AdvSearchEntryFilter(name: String, val type: Int) : Filter.Text(name)
class SortFilter : Filter.Select<SortType>("Sort by", SortType.values())
class LengthFilter : Filter.Select<LengthType>("Length", LengthType.values())
class MinimumRatingFilter : Filter.Select<String>("Minimum rating", (0 .. 5).map { "$it stars" }.toTypedArray())
class ExcludeParodiesFilter : Filter.CheckBox("Exclude parodies")
override val matchingHosts = listOf(
"www.tsumino.com"
"www.tsumino.com",
"tsumino.com"
)
override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
if(lcFirstPathSegment != "read" && lcFirstPathSegment != "book")
return null
return "https://tsumino.com/Book/Info/${uri.pathSegments[2]}"
}
private fun getArtists(document: Document): String {
val stringBuilder = StringBuilder()
val artists = document.select("#Artist a")
artists.forEach {
stringBuilder.append(it.text())
if (it != artists.last())
stringBuilder.append(", ")
}
return stringBuilder.toString()
}
private fun getDesc(document: Document): String {
val stringBuilder = StringBuilder()
val pages = document.select("#Pages").text()
val parodies = document.select("#Parody a")
val characters = document.select("#Character a")
val tags = document.select("#Tag a")
stringBuilder.append("Pages: $pages")
if (parodies.size > 0) {
stringBuilder.append("\n\n")
stringBuilder.append("Parodies: ")
parodies.forEach {
stringBuilder.append(it.text())
if (it != parodies.last())
stringBuilder.append(", ")
}
}
if (characters.size > 0) {
stringBuilder.append("\n\n")
stringBuilder.append("Characters: ")
characters.forEach {
stringBuilder.append(it.text())
if (it != characters.last())
stringBuilder.append(", ")
}
}
if (tags.size > 0) {
stringBuilder.append("\n\n")
stringBuilder.append("Tags: ")
tags.forEach {
stringBuilder.append(it.text())
if (it != tags.last())
stringBuilder.append(", ")
}
}
return stringBuilder.toString()
}
companion object {
val jsonParser by lazy {
JsonParser()
}
val jsonParser by lazy {JsonParser()}
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
private val ASP_NET_COOKIE_NAME = "ASP.NET_SessionId"
private val CAPTCHA_SCRIPT = """
|try{ document.querySelector('.tsumino-nav-btn').remove(); } catch(e) {}
|try{ document.querySelector('.tsumino-nav-title').href = '#' ;} catch(e) {}
|try{ document.querySelector('.tsumino-nav-items').remove() ;} catch(e) {}
""".trimMargin()
}
}

View File

@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino
/**
* Source helpers
@ -18,7 +19,7 @@ const val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6
const val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7
val HENTAI_CAFE_SOURCE_ID = delegatedSourceId<HentaiCafe>()
val PURURIN_SOURCE_ID = delegatedSourceId<Pururin>()
const val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9
val TSUMINO_SOURCE_ID = delegatedSourceId<Tsumino>()
const val HITOMI_SOURCE_ID = LEWD_SOURCE_SERIES + 10
const val EIGHTMUSES_SOURCE_ID = LEWD_SOURCE_SERIES + 11
const val HBROWSE_SOURCE_ID = LEWD_SOURCE_SERIES + 12
@ -26,7 +27,8 @@ const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
private val DELEGATED_LEWD_SOURCES = listOf(
HentaiCafe::class,
Pururin::class
Pururin::class,
Tsumino::class
)
val LIBRARY_UPDATE_EXCLUDED_SOURCES = listOf(

View File

@ -97,6 +97,21 @@ object EXHMigrations {
context.jobScheduler.cancelAll()
}
}
if (oldVersion < 8408) {
db.inTransaction {
// Migrate Tsumino source IDs
db.lowLevel().executeSQL(RawQuery.builder()
.query("""
UPDATE ${MangaTable.TABLE}
SET ${MangaTable.COL_SOURCE} = $TSUMINO_SOURCE_ID
WHERE ${MangaTable.COL_SOURCE} = 6909
""".trimIndent())
.affectsTables(MangaTable.TABLE)
.build())
}
}
// TODO BE CAREFUL TO NOT FUCK UP MergedSources IF CHANGING URLs
@ -118,6 +133,11 @@ object EXHMigrations {
manga.source = HENTAI_CAFE_SOURCE_ID
}
// Migrate Tsumino source IDs
if(manga.source == 6909L) {
manga.source = TSUMINO_SOURCE_ID
}
// Migrate nhentai URLs
if(manga.source == NHENTAI_SOURCE_ID) {
manga.url = getUrlWithoutDomain(manga.url)

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.jobScheduler
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.EXHMigrations
import exh.eh.EHentaiUpdateWorker
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
@ -29,6 +30,11 @@ object DebugFunctions {
val prefs: PreferencesHelper by injectLazy()
val sourceManager: SourceManager by injectLazy()
fun forceUpgradeMigration() {
prefs.eh_lastVersionCode().set(0)
EXHMigrations.upgrade(prefs)
}
fun resetAgedFlagInEXHManga() {
runBlocking {
val metadataManga = db.getFavoriteMangaWithMetadata().await()

View File

@ -77,7 +77,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
val BASE_URL = "https://www.tsumino.com"
fun tmIdFromUrl(url: String)
= Uri.parse(url).pathSegments[2]
= Uri.parse(url).lastPathSegment
fun mangaUrlFromId(id: String) = "/Book/Info/$id"