Attempt to add hitomi.la source (still broken) and code cleanup

This commit is contained in:
NerdNumber9
2018-03-13 15:21:31 -04:00
parent 07ce90ab8c
commit 87a2ac7887
12 changed files with 493 additions and 18 deletions

View File

@@ -144,4 +144,8 @@ object PreferenceKeys {
const val eh_ts_aspNetCookie = "eh_ts_aspNetCookie"
const val eh_showSettingsUploadWarning = "eh_showSettingsUploadWarning1"
const val eh_hl_refreshFrequency = "eh_nh_refresh_frequency"
const val eh_hl_lastRefresh = "eh_nh_last_refresh"
}

View File

@@ -222,5 +222,10 @@ class PreferencesHelper(val context: Context) {
fun eh_ts_aspNetCookie() = rxPrefs.getString(Keys.eh_ts_aspNetCookie, "")
fun eh_showSettingsUploadWarning() = rxPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true)
// Default is 24h, refresh daily
fun eh_hl_refreshFrequency() = rxPrefs.getString(Keys.eh_hl_refreshFrequency, "24")
fun eh_hl_lastRefresh() = rxPrefs.getLong(Keys.eh_hl_lastRefresh, 0L)
// <-- EH
}

View File

@@ -88,6 +88,8 @@ open class SourceManager(private val context: Context) {
exSrcs += NHentai(context)
exSrcs += HentaiCafe()
exSrcs += Tsumino(context)
// Mysteriously broken
// exSrcs += Hitomi(context)
return exSrcs
}
}

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SManga
import exh.metadata.models.GalleryQuery
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.SearchableGalleryMetadata
import exh.util.createUUIDObj
import exh.util.defRealm

View File

@@ -0,0 +1,309 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import com.github.salomonbrys.kotson.*
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.squareup.duktape.Duktape
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.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.HITOMI_SOURCE_ID
import exh.metadata.models.HitomiGalleryMetadata
import exh.metadata.models.HitomiGalleryMetadata.Companion.BASE_URL
import exh.metadata.models.HitomiGalleryMetadata.Companion.urlFromHlId
import exh.metadata.models.Tag
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.locks.ReentrantLock
class Hitomi(private val context: Context)
:HttpSource(), LewdSource<HitomiGalleryMetadata, HitomiGallery> {
override fun queryAll() = HitomiGalleryMetadata.EmptyQuery()
override fun queryFromUrl(url: String) = HitomiGalleryMetadata.UrlQuery(url)
override val metaParser: HitomiGalleryMetadata.(HitomiGallery) -> Unit = {
hlId = it.id.toString()
title = it.name
thumbnailUrl = resolveImage("//g.hitomi.la/galleries/$hlId/001.jpg")
artist = it.artists.firstOrNull()
group = it.groups.firstOrNull()
type = it.type
languageSimple = it.language
series.clear()
series.addAll(it.parodies)
characters.clear()
characters.addAll(it.characters)
tags.clear()
it.tags.mapTo(tags) { Tag(it.key, it.value) }
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Unused method called!")
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return loadGalleryMetadata(manga.url).map {
parseToManga(queryFromUrl(manga.url), it)
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return lazyLoadMeta(queryFromUrl(manga.url),
loadAllGalleryMetadata().map {
val mid = HitomiGalleryMetadata.hlIdFromUrl(manga.url)
it.find { it.id.toString() == mid }
}
).map {
listOf(SChapter.create().apply {
url = "$BASE_URL/reader/${it.hlId}.html"
name = "Chapter"
chapter_number = 1f
})
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override fun pageListParse(response: Response): List<Page> {
val doc = response.asJsoup()
return doc.select(".img-url").mapIndexed { index, element ->
val resolved = resolveImage(element.text())
Page(index, resolved, resolved)
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override val name = "hitomi.la"
override val baseUrl = BASE_URL
override val lang = "all"
override val id = HITOMI_SOURCE_ID
override val supportsLatest = true
private val prefs: PreferencesHelper by injectLazy()
private val jsonParser by lazy(LazyThreadSafetyMode.PUBLICATION) {
JsonParser()
}
private val cacheLock = ReentrantLock()
private var metaCache: List<HitomiGallery>? = null
override fun popularMangaRequest(page: Int) = GET("$BASE_URL/popular-all-$page.html")
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
override fun latestUpdatesRequest(page: Int) = GET("$BASE_URL/index-all-2.html")
private fun resolveMangaIds(doc: Document, data: List<HitomiGallery>): List<HitomiGallery> {
return doc.select(".gallery-content > div > a").mapNotNull {
val id = HitomiGalleryMetadata.hlIdFromUrl(it.attr("href"))
data.find { it.id.toString() == id }
}
}
private fun fetchAndResolveRequest(request: Request): Observable<MangasPage> {
return loadAllGalleryMetadata().flatMap {
client.newCall(request)
.asObservableSuccess()
.map { response ->
val doc = response.asJsoup()
val res = resolveMangaIds(doc, it)
val sManga = res.map {
parseToManga(queryFromUrl(urlFromHlId(it.id.toString())), it)
}
val hasNextPage = doc.select(".page-container > ul > li:last-child > a").isNotEmpty()
MangasPage(sManga, hasNextPage)
}
}
}
override fun fetchPopularManga(page: Int)
= fetchAndResolveRequest(popularMangaRequest(page))
override fun fetchLatestUpdates(page: Int)
= fetchAndResolveRequest(latestUpdatesRequest(page))
private fun galleryFile(index: Int)
= File(context.cacheDir.absoluteFile, "hitomi/galleries$index.json")
private fun shouldRefreshGalleryFiles(): Boolean {
val timeDiff = System.currentTimeMillis() - prefs.eh_hl_lastRefresh().getOrDefault()
return timeDiff > prefs.eh_hl_refreshFrequency().getOrDefault().toLong() * 60L * 60L * 1000L
}
private inline fun <T> lockCache(block: () -> T): T {
cacheLock.lock()
try {
return block()
} finally {
cacheLock.unlock()
}
}
private fun loadGalleryMetadata(url: String): Observable<HitomiGallery> {
return loadAllGalleryMetadata().map {
val mid = HitomiGalleryMetadata.hlIdFromUrl(url)
it.find { it.id.toString() == mid }
}
}
private fun loadAllGalleryMetadata(): Observable<List<HitomiGallery>> {
val shouldRefresh = shouldRefreshGalleryFiles()
metaCache?.let {
if(!shouldRefresh) {
return Observable.just(metaCache)
}
}
var obs: Observable<List<String>> = Observable.just(emptyList())
var refresh = false
for (i in 0 until GALLERY_CHUNK_COUNT) {
val cacheFile = galleryFile(i)
val newObs = if(shouldRefresh || !cacheFile.exists()) {
val url = "https://ltn.hitomi.la/galleries$i.json"
refresh = true
client.newCall(GET(url)).asObservableSuccess().map {
it.body()!!.string().apply {
lockCache {
cacheFile.parentFile.mkdirs()
cacheFile.writeText(this)
}
}
}
} else {
// Load galleries from cache
Observable.fromCallable {
lockCache {
cacheFile.readText()
}
}
}
obs = obs.flatMap { l ->
newObs.map {
l + it
}
}
}
// Update refresh time if we refreshed
if(refresh)
prefs.eh_hl_lastRefresh().set(System.currentTimeMillis())
return obs.map {
val res = it.flatMap {
jsonParser.parse(it).array.map {
HitomiGallery.fromJson(it.obj)
}
}
metaCache = res
res
}
}
private fun resolveImage(url: String): String {
return Duktape.create().use {
it.evaluate(IMAGE_RESOLVER.replace(IMAGE_RESOLVER_URL_VAR, url)) as String
}
}
companion object {
private val GALLERY_CHUNK_COUNT = 20
private val IMAGE_RESOLVER_URL_VAR = "%IMAGE_URL%"
private val IMAGE_RESOLVER = """
(function() {
var adapose = false; // Currently not sure what this does, it switches out frontend URL when we right click???
var number_of_frontends = 2;
function subdomain_from_galleryid(g) {
if (adapose) {
return '0';
}
return String.fromCharCode(97 + (g % number_of_frontends));
}
function subdomain_from_url(url, base) {
var retval = 'a';
if (base) {
retval = base;
}
var r = /\/(\d+)\//;
var m = r.exec(url);
var g;
if (m) {
g = parseInt(m[1]);
}
if (g) {
retval = subdomain_from_galleryid(g) + retval;
}
return retval;
}
function url_from_url(url, base) {
return url.replace(/\/\/..?\.hitomi\.la\//, '//'+subdomain_from_url(url, base)+'.hitomi.la/');
}
return url_from_url('$IMAGE_RESOLVER_URL_VAR');
})();
""".trimIndent()
}
}
data class HitomiGallery(val artists: List<String>,
val parodies: List<String>,
val id: Int,
val name: String,
val groups: List<String>,
val tags: Map<String, String>,
val characters: List<String>,
val type: String,
val language: String?) {
companion object {
fun fromJson(obj: JsonObject): HitomiGallery
= HitomiGallery(
obj.mapNullStringList("a"),
obj.mapNullStringList("p"),
obj["id"].int,
obj["n"].string,
obj.mapNullStringList("g"),
obj["t"]?.nullArray?.associate {
val str = it.string
if(str.contains(":"))
str.substringBefore(':') to str.substringAfter(':')
else
"tag" to str
} ?: emptyMap(),
obj.mapNullStringList("c"),
obj["type"].string,
obj["l"].nullString)
private fun JsonObject.mapNullStringList(key: String)
= this[key]?.nullArray?.map { it.string } ?: emptyList()
}
}

View File

@@ -31,6 +31,7 @@ class HentaiCafe : ParsedHttpSource(), LewdSource<HentaiCafeMetadata, Document>
override val name = "Hentai Cafe"
override val baseUrl = "https://hentai.cafe"
// Defer popular manga -> latest updates
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!")