Extract source api from app module (#8014)
* Extract source api from app module
* Extract source online api from app module
(cherry picked from commit 86fe850794)
# Conflicts:
# app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt
# core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
# source-api/src/main/java/eu/kanade/tachiyomi/source/Source.kt
# source-api/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
This commit is contained in:
@@ -20,6 +20,7 @@ import eu.kanade.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
|
||||
import eu.kanade.domain.manga.interactor.GetFavoriteEntries
|
||||
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
|
||||
import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.interactor.GetMangaBySource
|
||||
import eu.kanade.domain.manga.interactor.GetMergedManga
|
||||
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
||||
@@ -65,6 +66,7 @@ import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
|
||||
import eu.kanade.domain.source.interactor.ToggleSources
|
||||
import eu.kanade.domain.source.repository.FeedSavedSearchRepository
|
||||
import eu.kanade.domain.source.repository.SavedSearchRepository
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
@@ -100,6 +102,11 @@ class SYDomainModule : InjektModule {
|
||||
addFactory { ReorderSortTag(get(), get()) }
|
||||
addFactory { GetPagePreviews(get()) }
|
||||
|
||||
// Required for [MetadataSource]
|
||||
addFactory<MetadataSource.GetMangaId> { GetManga(get()) }
|
||||
addFactory<MetadataSource.GetFlatMetadataById> { GetFlatMetadataById(get()) }
|
||||
addFactory<MetadataSource.InsertFlatMetadata> { InsertFlatMetadata(get()) }
|
||||
|
||||
addSingletonFactory<MangaMetadataRepository> { MangaMetadataRepositoryImpl(get()) }
|
||||
addFactory { GetFlatMetadataById(get()) }
|
||||
addFactory { InsertFlatMetadata(get()) }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.repository.MangaMetadataRepository
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.metadata.metadata.base.FlatMetadata
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -9,9 +10,9 @@ import logcat.LogPriority
|
||||
|
||||
class GetFlatMetadataById(
|
||||
private val mangaMetadataRepository: MangaMetadataRepository,
|
||||
) {
|
||||
) : MetadataSource.GetFlatMetadataById {
|
||||
|
||||
suspend fun await(id: Long): FlatMetadata? {
|
||||
override suspend fun await(id: Long): FlatMetadata? {
|
||||
return try {
|
||||
val meta = mangaMetadataRepository.getMetadataById(id)
|
||||
return if (meta != null) {
|
||||
|
||||
@@ -2,13 +2,14 @@ package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
class GetManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
) : MetadataSource.GetMangaId {
|
||||
|
||||
suspend fun await(id: Long): Manga? {
|
||||
return try {
|
||||
@@ -30,4 +31,10 @@ class GetManga(
|
||||
fun subscribe(url: String, sourceId: Long): Flow<Manga?> {
|
||||
return mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override suspend fun awaitId(url: String, sourceId: Long): Long? {
|
||||
return await(url, sourceId)?.id
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.repository.MangaMetadataRepository
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.metadata.metadata.base.FlatMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
@@ -8,7 +9,7 @@ import logcat.LogPriority
|
||||
|
||||
class InsertFlatMetadata(
|
||||
private val mangaMetadataRepository: MangaMetadataRepository,
|
||||
) {
|
||||
) : MetadataSource.InsertFlatMetadata {
|
||||
|
||||
suspend fun await(flatMetadata: FlatMetadata) {
|
||||
try {
|
||||
@@ -18,7 +19,7 @@ class InsertFlatMetadata(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(metadata: RaisedSearchMetadata) {
|
||||
override suspend fun await(metadata: RaisedSearchMetadata) {
|
||||
try {
|
||||
mangaMetadataRepository.insertMetadata(metadata)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -72,14 +72,33 @@ import eu.kanade.presentation.util.isScrollingUp
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
||||
import eu.kanade.tachiyomi.source.online.english.HBrowse
|
||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import exh.ui.metadata.adapters.EHentaiDescription
|
||||
import exh.ui.metadata.adapters.EightMusesDescription
|
||||
import exh.ui.metadata.adapters.HBrowseDescription
|
||||
import exh.ui.metadata.adapters.HitomiDescription
|
||||
import exh.ui.metadata.adapters.MangaDexDescription
|
||||
import exh.ui.metadata.adapters.NHentaiDescription
|
||||
import exh.ui.metadata.adapters.PervEdenDescription
|
||||
import exh.ui.metadata.adapters.PururinDescription
|
||||
import exh.ui.metadata.adapters.TsuminoDescription
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
@@ -255,7 +274,7 @@ private fun MangaScreenSmallImpl(
|
||||
|
||||
val chapters = remember(state) { state.processedChapters.toList() }
|
||||
// SY -->
|
||||
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
|
||||
val metadataDescription = metadataDescription(state.source)
|
||||
// SY <--
|
||||
|
||||
val internalOnBackPressed = {
|
||||
@@ -408,12 +427,12 @@ private fun MangaScreenSmallImpl(
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (metadataSource != null) {
|
||||
if (metadataDescription != null) {
|
||||
item(
|
||||
key = MangaScreenItem.METADATA_INFO,
|
||||
contentType = MangaScreenItem.METADATA_INFO,
|
||||
) {
|
||||
metadataSource.DescriptionComposable(
|
||||
metadataDescription(
|
||||
state = state,
|
||||
openMetadataViewer = onMetadataViewerClicked,
|
||||
search = { onSearch(it, false) },
|
||||
@@ -540,7 +559,7 @@ fun MangaScreenLargeImpl(
|
||||
val chapters = remember(state) { state.processedChapters.toList() }
|
||||
|
||||
// SY -->
|
||||
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
|
||||
val metadataDescription = metadataDescription(state.source)
|
||||
// SY <--
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
@@ -681,7 +700,7 @@ fun MangaScreenLargeImpl(
|
||||
// SY <--
|
||||
)
|
||||
// SY -->
|
||||
metadataSource?.DescriptionComposable(
|
||||
metadataDescription?.invoke(
|
||||
state = state,
|
||||
openMetadataViewer = onMetadataViewerClicked,
|
||||
search = { onSearch(it, false) },
|
||||
@@ -847,3 +866,42 @@ private fun onChapterItemClick(
|
||||
else -> onChapterClicked(chapterItem.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
typealias MetadataDescriptionComposable = @Composable (state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) -> Unit
|
||||
|
||||
@Composable
|
||||
fun metadataDescription(source: Source): MetadataDescriptionComposable? {
|
||||
val metadataSource = remember(source.id) { source.getMainSource<MetadataSource<*, *>>() }
|
||||
return remember(metadataSource) {
|
||||
when (metadataSource) {
|
||||
is EHentai -> { state, openMetadataViewer, search ->
|
||||
EHentaiDescription(state, openMetadataViewer, search)
|
||||
}
|
||||
is Hitomi -> { state, openMetadataViewer, _ ->
|
||||
HitomiDescription(state, openMetadataViewer)
|
||||
}
|
||||
is MangaDex -> { state, openMetadataViewer, _ ->
|
||||
MangaDexDescription(state, openMetadataViewer)
|
||||
}
|
||||
is NHentai -> { state, openMetadataViewer, _ ->
|
||||
NHentaiDescription(state, openMetadataViewer)
|
||||
}
|
||||
is PervEden -> { state, openMetadataViewer, _ ->
|
||||
PervEdenDescription(state, openMetadataViewer)
|
||||
}
|
||||
is EightMuses -> { state, openMetadataViewer, _ ->
|
||||
EightMusesDescription(state, openMetadataViewer)
|
||||
}
|
||||
is HBrowse -> { state, openMetadataViewer, _ ->
|
||||
HBrowseDescription(state, openMetadataViewer)
|
||||
}
|
||||
is Pururin -> { state, openMetadataViewer, _ ->
|
||||
PururinDescription(state, openMetadataViewer)
|
||||
}
|
||||
is Tsumino -> { state, openMetadataViewer, _ ->
|
||||
TsuminoDescription(state, openMetadataViewer)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.copyFrom
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toLong
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class MangaImpl : Manga {
|
||||
@@ -77,6 +81,19 @@ open class MangaImpl : Manga {
|
||||
private set
|
||||
var ogStatus: Int = 0
|
||||
private set
|
||||
|
||||
override val originalTitle: String
|
||||
get() = ogTitle
|
||||
override val originalAuthor: String?
|
||||
get() = ogAuthor ?: author
|
||||
override val originalArtist: String?
|
||||
get() = ogArtist ?: artist
|
||||
override val originalDescription: String?
|
||||
get() = ogDesc ?: description
|
||||
override val originalGenre: String?
|
||||
get() = ogGenre ?: genre
|
||||
override val originalStatus: Int
|
||||
get() = ogStatus
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -93,6 +110,18 @@ open class MangaImpl : Manga {
|
||||
}
|
||||
|
||||
// SY -->
|
||||
override fun copyFrom(other: SManga) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank() && originalTitle != other.title) {
|
||||
val source = (this as? Manga)?.source
|
||||
if (source != null) {
|
||||
Injekt.get<DownloadManager>().renameMangaDir(originalTitle, other.originalTitle, source)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
super.copyFrom(other)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class AndroidCookieJar : CookieJar {
|
||||
|
||||
private val manager = CookieManager.getInstance()
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val urlString = url.toString()
|
||||
|
||||
cookies.forEach { manager.setCookie(urlString, it.toString()) }
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return get(url)
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> {
|
||||
val cookies = manager.getCookie(url.toString())
|
||||
|
||||
return if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1): Int {
|
||||
val urlString = url.toString()
|
||||
val cookies = manager.getCookie(urlString) ?: return 0
|
||||
|
||||
fun List<String>.filterNames(): List<String> {
|
||||
return if (cookieNames != null) {
|
||||
this.filter { it in cookieNames }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
return cookies.split(";")
|
||||
.map { it.substringBefore("=") }
|
||||
.filterNames()
|
||||
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
|
||||
.count()
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
manager.removeAllCookies {}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import java.net.InetAddress
|
||||
|
||||
/**
|
||||
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
|
||||
*/
|
||||
|
||||
const val PREF_DOH_CLOUDFLARE = 1
|
||||
const val PREF_DOH_GOOGLE = 2
|
||||
const val PREF_DOH_ADGUARD = 3
|
||||
const val PREF_DOH_QUAD9 = 4
|
||||
const val PREF_DOH_ALIDNS = 5
|
||||
const val PREF_DOH_DNSPOD = 6
|
||||
const val PREF_DOH_360 = 7
|
||||
const val PREF_DOH_QUAD101 = 8
|
||||
const val PREF_DOH_MULLVAD = 9
|
||||
const val PREF_DOH_CONTROLD = 10
|
||||
const val PREF_DOH_NJALLA = 11
|
||||
|
||||
fun OkHttpClient.Builder.dohCloudflare() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("162.159.36.1"),
|
||||
InetAddress.getByName("162.159.46.1"),
|
||||
InetAddress.getByName("1.1.1.1"),
|
||||
InetAddress.getByName("1.0.0.1"),
|
||||
InetAddress.getByName("162.159.132.53"),
|
||||
InetAddress.getByName("2606:4700:4700::1111"),
|
||||
InetAddress.getByName("2606:4700:4700::1001"),
|
||||
InetAddress.getByName("2606:4700:4700::0064"),
|
||||
InetAddress.getByName("2606:4700:4700::6400"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohGoogle() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.google/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("8.8.4.4"),
|
||||
InetAddress.getByName("8.8.8.8"),
|
||||
InetAddress.getByName("2001:4860:4860::8888"),
|
||||
InetAddress.getByName("2001:4860:4860::8844"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
// AdGuard "Default" DNS works too but for the sake of making sure no site is blacklisted,
|
||||
// we use "Unfiltered"
|
||||
fun OkHttpClient.Builder.dohAdGuard() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("94.140.14.140"),
|
||||
InetAddress.getByName("94.140.14.141"),
|
||||
InetAddress.getByName("2a10:50c0::1:ff"),
|
||||
InetAddress.getByName("2a10:50c0::2:ff"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohQuad9() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.quad9.net/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("9.9.9.9"),
|
||||
InetAddress.getByName("149.112.112.112"),
|
||||
InetAddress.getByName("2620:fe::fe"),
|
||||
InetAddress.getByName("2620:fe::9"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohAliDNS() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.alidns.com/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("223.5.5.5"),
|
||||
InetAddress.getByName("223.6.6.6"),
|
||||
InetAddress.getByName("2400:3200::1"),
|
||||
InetAddress.getByName("2400:3200:baba::1"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohDNSPod() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.pub/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("1.12.12.12"),
|
||||
InetAddress.getByName("120.53.53.53"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.doh360() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.360.cn/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("101.226.4.6"),
|
||||
InetAddress.getByName("218.30.118.6"),
|
||||
InetAddress.getByName("123.125.81.6"),
|
||||
InetAddress.getByName("140.207.198.6"),
|
||||
InetAddress.getByName("180.163.249.75"),
|
||||
InetAddress.getByName("101.199.113.208"),
|
||||
InetAddress.getByName("36.99.170.86"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.dohQuad101() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("101.101.101.101"),
|
||||
InetAddress.getByName("2001:de4::101"),
|
||||
InetAddress.getByName("2001:de4::102"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
/*
|
||||
* Mullvad DoH
|
||||
* without ad blocking option
|
||||
* Source : https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
|
||||
*/
|
||||
fun OkHttpClient.Builder.dohMullvad() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://doh.mullvad.net/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("194.242.2.2"),
|
||||
InetAddress.getByName("193.19.108.2"),
|
||||
InetAddress.getByName("2a07:e340::2"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
/*
|
||||
* Control D
|
||||
* unfiltered option
|
||||
* Source : https://controld.com/free-dns/?
|
||||
*/
|
||||
|
||||
fun OkHttpClient.Builder.dohControlD() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://freedns.controld.com/p0".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("76.76.2.0"),
|
||||
InetAddress.getByName("76.76.10.0"),
|
||||
InetAddress.getByName("2606:1a40::"),
|
||||
InetAddress.getByName("2606:1a40:1::"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
/*
|
||||
* Njalla
|
||||
*
|
||||
* Non logging and uncensored
|
||||
*/
|
||||
fun OkHttpClient.Builder.dohNajalla() = dns(
|
||||
DnsOverHttps.Builder().client(build())
|
||||
.url("https://dns.njal.la/dns-query".toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
InetAddress.getByName("95.215.19.53"),
|
||||
InetAddress.getByName("2001:67c:2354:2::53"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
@@ -1,79 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.Http103Interceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/* SY --> */
|
||||
open /* SY <-- */ class NetworkHelper(context: Context) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
/* SY --> */
|
||||
open /* SY <-- */val cookieManager = AndroidCookieJar()
|
||||
|
||||
private val userAgentInterceptor by lazy { UserAgentInterceptor() }
|
||||
private val http103Interceptor by lazy { Http103Interceptor(context) }
|
||||
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) }
|
||||
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.fastFallback(true)
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
.addNetworkInterceptor(http103Interceptor)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
when (preferences.dohProvider()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
PREF_DOH_QUAD9 -> builder.dohQuad9()
|
||||
PREF_DOH_ALIDNS -> builder.dohAliDNS()
|
||||
PREF_DOH_DNSPOD -> builder.dohDNSPod()
|
||||
PREF_DOH_360 -> builder.doh360()
|
||||
PREF_DOH_QUAD101 -> builder.dohQuad101()
|
||||
PREF_DOH_MULLVAD -> builder.dohMullvad()
|
||||
PREF_DOH_CONTROLD -> builder.dohControlD()
|
||||
PREF_DOH_NJALLA -> builder.dohNajalla()
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
/* SY --> */
|
||||
open /* SY <-- */val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
|
||||
/* SY --> */
|
||||
open /* SY <-- */val cloudflareClient by lazy {
|
||||
client.newBuilder()
|
||||
.addInterceptor(cloudflareInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
val defaultUserAgent by lazy {
|
||||
preferences.defaultUserAgent().get()
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.fullType
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||
val call = clone()
|
||||
|
||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||
override fun request(n: Long) {
|
||||
if (n == 0L || !compareAndSet(false, true)) return
|
||||
|
||||
try {
|
||||
val response = call.execute()
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onNext(response)
|
||||
subscriber.onCompleted()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
return call.isCanceled()
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.add(requestArbiter)
|
||||
subscriber.setProducer(requestArbiter)
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
continuation.resumeWithException(HttpException(response.code))
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(response) {
|
||||
response.body.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
cancel()
|
||||
} catch (ex: Throwable) {
|
||||
// Ignore cancel exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body, listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
return progressClient.newCall(request)
|
||||
}
|
||||
|
||||
inline fun <reified T> Response.parseAs(/* SY --> */ json: Json = Injekt.getInstance(fullType<Json>().type) /* SY <-- */): T {
|
||||
// Avoiding Injekt.get<Json>() due to compiler issues
|
||||
// val json = Injekt.getInstance<Json>(fullType<Json>().type)
|
||||
this.use {
|
||||
val responseBody = it.body.string()
|
||||
return json.decodeFromString(responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
||||
@@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
source(responseBody.source()).buffer()
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
return bufferedSource
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||
internal val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
|
||||
|
||||
fun GET(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun POST(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun PUT(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.put(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun DELETE(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.delete(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) {
|
||||
|
||||
private val executor = ContextCompat.getMainExecutor(context)
|
||||
|
||||
private val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
override fun shouldIntercept(response: Response): Boolean {
|
||||
// Check if Cloudflare anti-bot is on
|
||||
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||
try {
|
||||
response.close()
|
||||
networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0)
|
||||
val oldCookie = networkHelper.cookieManager.get(request.url)
|
||||
.firstOrNull { it.name == "cf_clearance" }
|
||||
resolveWithWebView(request, oldCookie)
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
// we don't crash the entire app
|
||||
catch (e: CloudflareBypassException) {
|
||||
throw IOException(context.getString(R.string.information_cloudflare_bypass_failure))
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) {
|
||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||
// OkHttp doesn't support asynchronous interceptors.
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
var webView: WebView? = null
|
||||
|
||||
var challengeFound = false
|
||||
var cloudflareBypassed = false
|
||||
var isWebViewOutdated = false
|
||||
|
||||
val origRequestUrl = originalRequest.url.toString()
|
||||
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
|
||||
executor.execute {
|
||||
val webview = WebView(context)
|
||||
webView = webview
|
||||
webview.setDefaultSettings()
|
||||
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
webview.settings.userAgentString = originalRequest.header("User-Agent")
|
||||
?: networkHelper.defaultUserAgent
|
||||
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
fun isCloudFlareBypassed(): Boolean {
|
||||
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
|
||||
.firstOrNull { it.name == "cf_clearance" }
|
||||
.let { it != null && it != oldCookie }
|
||||
}
|
||||
|
||||
if (isCloudFlareBypassed()) {
|
||||
cloudflareBypassed = true
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
if (url == origRequestUrl && !challengeFound) {
|
||||
// The first request didn't return the challenge, abort.
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean,
|
||||
) {
|
||||
if (isMainFrame) {
|
||||
if (errorCode in ERROR_CODES) {
|
||||
// Found the Cloudflare challenge page.
|
||||
challengeFound = true
|
||||
} else {
|
||||
// Unlock thread, the challenge wasn't found.
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webView?.loadUrl(origRequestUrl, headers)
|
||||
}
|
||||
|
||||
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
|
||||
// around 4 seconds but it can take more due to slow networks or server issues.
|
||||
latch.await(12, TimeUnit.SECONDS)
|
||||
|
||||
executor.execute {
|
||||
if (!cloudflareBypassed) {
|
||||
isWebViewOutdated = webView?.isOutdated() == true
|
||||
}
|
||||
|
||||
webView?.stopLoading()
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
}
|
||||
|
||||
// Throw exception if we failed to bypass Cloudflare
|
||||
if (!cloudflareBypassed) {
|
||||
// Prompt user to update WebView if it seems too outdated
|
||||
if (isWebViewOutdated) {
|
||||
context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
throw CloudflareBypassException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ERROR_CODES = listOf(403, 503)
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
|
||||
private class CloudflareBypassException : Exception()
|
||||
@@ -1,112 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO: Remove when OkHttp can handle HTTP 103 responses
|
||||
class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
|
||||
|
||||
private val executor = ContextCompat.getMainExecutor(context)
|
||||
|
||||
override fun shouldIntercept(response: Response): Boolean {
|
||||
return response.code == 103
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||
logcat { "Proceeding with WebView for request $request" }
|
||||
try {
|
||||
return proceedWithWebView(request, response)
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||
private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
|
||||
// We need to lock this thread until the WebView loads the page, because
|
||||
// OkHttp doesn't support asynchronous interceptors.
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
val jsInterface = JsInterface(latch)
|
||||
|
||||
var outerWebView: WebView? = null
|
||||
|
||||
var exception: Exception? = null
|
||||
|
||||
val requestUrl = originalRequest.url.toString()
|
||||
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
|
||||
executor.execute {
|
||||
val webview = createWebView(originalRequest).also { outerWebView = it }
|
||||
webview.addJavascriptInterface(jsInterface, "android")
|
||||
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
view.evaluateJavascript(jsScript) {}
|
||||
}
|
||||
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean,
|
||||
) {
|
||||
if (isMainFrame) {
|
||||
exception = Exception("Error $errorCode - $description")
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webview.loadUrl(requestUrl, headers)
|
||||
}
|
||||
|
||||
latch.await(10, TimeUnit.SECONDS)
|
||||
|
||||
executor.execute {
|
||||
outerWebView?.run {
|
||||
stopLoading()
|
||||
destroy()
|
||||
}
|
||||
outerWebView = null
|
||||
}
|
||||
|
||||
exception?.let { throw it }
|
||||
|
||||
val responseHtml = jsInterface.responseHtml ?: throw Exception("Couldn't fetch site through webview")
|
||||
|
||||
return originalResponse.newBuilder()
|
||||
.code(200)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.message("OK")
|
||||
.body(responseHtml.toResponseBody(htmlMediaType))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
internal class JsInterface(private val latch: CountDownLatch, var responseHtml: String? = null) {
|
||||
@Suppress("UNUSED")
|
||||
@JavascriptInterface
|
||||
fun passPayload(passedPayload: String) {
|
||||
responseHtml = passedPayload
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
private const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
|
||||
private val htmlMediaType = "text/html".toMediaType()
|
||||
@@ -1,105 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
fun OkHttpClient.Builder.rateLimit(
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
|
||||
|
||||
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
internal class RateLimitInterceptor(
|
||||
private val host: String?,
|
||||
private val permits: Int,
|
||||
period: Long,
|
||||
unit: TimeUnit,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayDeque<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val fairLock = Semaphore(1, true)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val call = chain.call()
|
||||
if (call.isCanceled()) throw IOException("Canceled")
|
||||
|
||||
val request = chain.request()
|
||||
when (host) {
|
||||
null, request.url.host -> {} // need rate limit
|
||||
else -> return chain.proceed(request)
|
||||
}
|
||||
|
||||
try {
|
||||
fairLock.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
||||
val requestQueue = this.requestQueue
|
||||
val timestamp: Long
|
||||
|
||||
try {
|
||||
synchronized(requestQueue) {
|
||||
while (requestQueue.size >= permits) { // queue is full, remove expired entries
|
||||
val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
|
||||
var hasRemovedExpired = false
|
||||
while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
|
||||
requestQueue.removeFirst()
|
||||
hasRemovedExpired = true
|
||||
}
|
||||
if (call.isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
} else if (hasRemovedExpired) {
|
||||
break
|
||||
} else {
|
||||
try { // wait for the first entry to expire, or notified by cached response
|
||||
(requestQueue as Object).wait(requestQueue.first - periodStart)
|
||||
} catch (_: InterruptedException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add request to queue
|
||||
timestamp = SystemClock.elapsedRealtime()
|
||||
requestQueue.addLast(timestamp)
|
||||
}
|
||||
} finally {
|
||||
fairLock.release()
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
if (response.networkResponse == null) { // response is cached, remove it from queue
|
||||
synchronized(requestQueue) {
|
||||
if (requestQueue.isEmpty() || timestamp < requestQueue.first) return@synchronized
|
||||
requestQueue.removeFirstOccurrence(timestamp)
|
||||
(requestQueue as Object).notifyAll()
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*
|
||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
fun OkHttpClient.Builder.rateLimitHost(
|
||||
httpUrl: HttpUrl,
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit))
|
||||
@@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class UserAgentInterceptor : Interceptor {
|
||||
|
||||
private val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
|
||||
val newRequest = originalRequest
|
||||
.newBuilder()
|
||||
.removeHeader("User-Agent")
|
||||
.addHeader("User-Agent", networkHelper.defaultUserAgent)
|
||||
.build()
|
||||
chain.proceed(newRequest)
|
||||
} else {
|
||||
chain.proceed(originalRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
private val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
||||
* Application class.
|
||||
*/
|
||||
private val initWebView by lazy {
|
||||
// Crashes on some devices. We skip this in some cases since the only impact is slower
|
||||
// WebView init in those rare cases.
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
|
||||
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
|
||||
return@lazy
|
||||
}
|
||||
|
||||
WebSettings.getDefaultUserAgent(context)
|
||||
}
|
||||
|
||||
abstract fun shouldIntercept(response: Response): Boolean
|
||||
|
||||
abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
if (!shouldIntercept(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (!WebViewUtil.supportsWebView(context)) {
|
||||
launchUI {
|
||||
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
}
|
||||
return response
|
||||
}
|
||||
initWebView
|
||||
|
||||
return intercept(chain, request, response)
|
||||
}
|
||||
|
||||
fun createWebView(request: Request): WebView {
|
||||
val webview = WebView(context)
|
||||
webview.setDefaultSettings()
|
||||
webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent
|
||||
return webview
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
|
||||
interface CatalogueSource : Source {
|
||||
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
override val lang: String
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
fun getFilterList(): FilterList
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
|
||||
interface ConfigurableSource : Source {
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Response
|
||||
|
||||
interface PagePreviewSource : Source {
|
||||
|
||||
suspend fun getPagePreviewList(manga: SManga, page: Int): PagePreviewPage
|
||||
|
||||
suspend fun fetchPreviewImage(page: PagePreviewInfo, cacheControl: CacheControl? = null): Response
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PagePreviewPage(
|
||||
val page: Int,
|
||||
val pagePreviews: List<PagePreviewInfo>,
|
||||
val hasNextPage: Boolean,
|
||||
val pagePreviewPages: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PagePreviewInfo(
|
||||
val index: Int,
|
||||
val imageUrl: String,
|
||||
@Transient
|
||||
private val _progress: MutableStateFlow<Int> = MutableStateFlow(-1),
|
||||
) : ProgressListener {
|
||||
@Transient
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
_progress.value = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.model.SourceData
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
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.util.lang.awaitSingle
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||
*/
|
||||
interface Source {
|
||||
|
||||
/**
|
||||
* Id for the source. Must be unique.
|
||||
*/
|
||||
val id: Long
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
val lang: String
|
||||
get() = ""
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the 1.x API instead",
|
||||
ReplaceWith("getMangaDetails"),
|
||||
)
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Returns an observable with all the available chapters for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the 1.x API instead",
|
||||
ReplaceWith("getChapterList"),
|
||||
)
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
||||
|
||||
// TODO: remove direct usages on this method
|
||||
/**
|
||||
* Returns an observable with the list of pages a chapter has.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the 1.x API instead",
|
||||
ReplaceWith("getPageList"),
|
||||
)
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
|
||||
|
||||
/**
|
||||
* [1.x API] Get the updated details for a manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get all the available chapters for a manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return fetchChapterList(manga).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get the list of pages a chapter has.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
fun Source.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
|
||||
|
||||
fun Source.getNameForMangaInfo(mergeSources: List<Source>?): String {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val enabledLanguages = preferences.enabledLanguages().get()
|
||||
.filterNot { it in listOf("all", "other") }
|
||||
val hasOneActiveLanguages = enabledLanguages.size == 1
|
||||
val isInEnabledLanguages = lang in enabledLanguages
|
||||
return when {
|
||||
// SY -->
|
||||
!mergeSources.isNullOrEmpty() -> getMergedSourcesString(
|
||||
mergeSources,
|
||||
enabledLanguages,
|
||||
hasOneActiveLanguages,
|
||||
)
|
||||
// SY <--
|
||||
// For edge cases where user disables a source they got manga of in their library.
|
||||
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
|
||||
// Hide the language tag when only one language is used.
|
||||
hasOneActiveLanguages && isInEnabledLanguages -> name
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun getMergedSourcesString(
|
||||
mergeSources: List<Source>,
|
||||
enabledLangs: List<String>,
|
||||
onlyName: Boolean,
|
||||
): String {
|
||||
return if (onlyName) {
|
||||
mergeSources.joinToString { source ->
|
||||
if (source.lang !in enabledLangs) {
|
||||
source.toString()
|
||||
} else {
|
||||
source.name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergeSources.joinToString()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource
|
||||
@@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.model.SourceData
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
fun Source.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
|
||||
|
||||
fun Source.getNameForMangaInfo(mergeSources: List<Source>?): String {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val enabledLanguages = preferences.enabledLanguages().get()
|
||||
.filterNot { it in listOf("all", "other") }
|
||||
val hasOneActiveLanguages = enabledLanguages.size == 1
|
||||
val isInEnabledLanguages = lang in enabledLanguages
|
||||
return when {
|
||||
// SY -->
|
||||
!mergeSources.isNullOrEmpty() -> getMergedSourcesString(
|
||||
mergeSources,
|
||||
enabledLanguages,
|
||||
hasOneActiveLanguages,
|
||||
)
|
||||
// SY <--
|
||||
// For edge cases where user disables a source they got manga of in their library.
|
||||
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
|
||||
// Hide the language tag when only one language is used.
|
||||
hasOneActiveLanguages && isInEnabledLanguages -> name
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun getMergedSourcesString(
|
||||
mergeSources: List<Source>,
|
||||
enabledLangs: List<String>,
|
||||
onlyName: Boolean,
|
||||
): String {
|
||||
return if (onlyName) {
|
||||
mergeSources.joinToString { source ->
|
||||
if (source.lang !in enabledLangs) {
|
||||
source.toString()
|
||||
} else {
|
||||
source.name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergeSources.joinToString()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource
|
||||
@@ -1,12 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
/**
|
||||
* A factory for creating sources at runtime.
|
||||
*/
|
||||
interface SourceFactory {
|
||||
/**
|
||||
* Create a new copy of the sources
|
||||
* @return The created sources
|
||||
*/
|
||||
fun createSources(): List<Source>
|
||||
}
|
||||
@@ -133,6 +133,7 @@ class SourceManager(
|
||||
val enhancedSource = EnhancedHttpSource(
|
||||
this,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(this, context),
|
||||
::delegateSources,
|
||||
)
|
||||
|
||||
currentDelegatedSources[enhancedSource.originalSource.id] = DelegatedSource(
|
||||
@@ -156,6 +157,8 @@ class SourceManager(
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
private fun delegateSources() = preferences.delegateSources().get()
|
||||
|
||||
fun get(sourceKey: Long): Source? {
|
||||
return sourcesMap[sourceKey]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
/**
|
||||
* A source that explicitly doesn't require traffic considerations.
|
||||
*
|
||||
* This typically applies for self-hosted sources.
|
||||
*/
|
||||
interface UnmeteredSource
|
||||
@@ -1,51 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
open class Separator(name: String = "") : Filter<Any>(name, 0)
|
||||
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
|
||||
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
|
||||
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
|
||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
|
||||
fun isIgnored() = state == STATE_IGNORE
|
||||
fun isIncluded() = state == STATE_INCLUDE
|
||||
fun isExcluded() = state == STATE_EXCLUDE
|
||||
|
||||
companion object {
|
||||
const val STATE_IGNORE = 0
|
||||
const val STATE_INCLUDE = 1
|
||||
const val STATE_EXCLUDE = 2
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Group<V>(name: String, state: List<V>) : Filter<List<V>>(name, state)
|
||||
|
||||
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
|
||||
Filter<Sort.Selection?>(name, state) {
|
||||
data class Selection(val index: Int, val ascending: Boolean)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
abstract class AutoComplete(
|
||||
name: String,
|
||||
val hint: String,
|
||||
val values: List<String>,
|
||||
val skipAutoFillTags: List<String> = emptyList(),
|
||||
val excludePrefix: String? = null,
|
||||
state: List<String>,
|
||||
) : Filter<List<String>>(name, state)
|
||||
// SY <--
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Filter<*>) return false
|
||||
|
||||
return name == other.name && state == other.state
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + (state?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return list.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
|
||||
/* SY --> */
|
||||
open /* SY <-- */ class MangasPage(open val mangas: List<SManga>, open val hasNextPage: Boolean) {
|
||||
// SY -->
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is MangasPage) return false
|
||||
|
||||
if (mangas != other.mangas) return false
|
||||
if (hasNextPage != other.hasNextPage) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = mangas.hashCode()
|
||||
result = 31 * result + hasNextPage.hashCode()
|
||||
return result
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun copy(mangas: List<SManga> = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage {
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
data class MetadataMangasPage(override val mangas: List<SManga>, override val hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
|
||||
// SY <--
|
||||
@@ -1,67 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import rx.subjects.Subject
|
||||
|
||||
@Serializable
|
||||
open class Page(
|
||||
val index: Int,
|
||||
/* SY --> */
|
||||
var /* SY <-- */ url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions
|
||||
) : ProgressListener {
|
||||
|
||||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var status: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var progress: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
@Transient
|
||||
private var statusCallback: ((Page) -> Unit)? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Page) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import data.Chapters
|
||||
import java.io.Serializable
|
||||
|
||||
interface SChapter : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var name: String
|
||||
|
||||
var date_upload: Long
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
var scanlator: String?
|
||||
|
||||
fun copyFrom(other: SChapter) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
date_upload = other.date_upload
|
||||
chapter_number = other.chapter_number
|
||||
scanlator = other.scanlator
|
||||
}
|
||||
|
||||
fun copyFrom(other: Chapters) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
date_upload = other.date_upload
|
||||
chapter_number = other.chapter_number
|
||||
scanlator = other.scanlator
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): SChapter {
|
||||
return SChapterImpl()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
operator fun invoke(
|
||||
name: String,
|
||||
url: String,
|
||||
date_upload: Long = 0,
|
||||
chapter_number: Float = -1F,
|
||||
scanlator: String? = null,
|
||||
): SChapter {
|
||||
return create().apply {
|
||||
this.name = name
|
||||
this.url = url
|
||||
this.date_upload = date_upload
|
||||
this.chapter_number = chapter_number
|
||||
this.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import data.Chapters
|
||||
|
||||
fun SChapter.copyFrom(other: Chapters) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
date_upload = other.date_upload
|
||||
chapter_number = other.chapter_number
|
||||
scanlator = other.scanlator
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
override lateinit var name: String
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var chapter_number: Float = -1f
|
||||
|
||||
override var scanlator: String? = null
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import data.Mangas
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var title: String
|
||||
|
||||
var artist: String?
|
||||
|
||||
var author: String?
|
||||
|
||||
var description: String?
|
||||
|
||||
var genre: String?
|
||||
|
||||
var status: Int
|
||||
|
||||
var thumbnail_url: String?
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
if (genre.isNullOrBlank()) return null
|
||||
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
val originalTitle: String
|
||||
get() = (this as? MangaImpl)?.ogTitle ?: title
|
||||
val originalAuthor: String?
|
||||
get() = (this as? MangaImpl)?.ogAuthor ?: author
|
||||
val originalArtist: String?
|
||||
get() = (this as? MangaImpl)?.ogArtist ?: artist
|
||||
val originalDescription: String?
|
||||
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||
val originalGenre: String?
|
||||
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||
val originalStatus: Int
|
||||
get() = (this as? MangaImpl)?.ogStatus ?: status
|
||||
// SY <--
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank() && originalTitle != other.title) {
|
||||
val oldTitle = originalTitle
|
||||
title = other.originalTitle
|
||||
val source = (this as? Manga)?.source
|
||||
if (source != null) {
|
||||
Injekt.get<DownloadManager>().renameMangaDir(oldTitle, other.originalTitle, source)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (other.author != null) {
|
||||
author = /* SY --> */ other.originalAuthor // SY <--
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = /* SY --> */ other.originalArtist // SY <--
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = /* SY --> */ other.originalDescription // SY <--
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = /* SY --> */ other.originalGenre // SY <--
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
status = other.status
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
|
||||
fun copyFrom(other: Mangas) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank() && originalTitle != other.title) {
|
||||
val oldTitle = originalTitle
|
||||
title = other.title
|
||||
val source = (this as? Manga)?.source
|
||||
if (source != null) {
|
||||
Injekt.get<DownloadManager>().renameMangaDir(oldTitle, other.title, source)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre.joinToString(separator = ", ")
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
status = other.status.toInt()
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
|
||||
fun copy() = create().also {
|
||||
it.url = url
|
||||
// SY -->
|
||||
it.title = originalTitle
|
||||
it.artist = originalArtist
|
||||
it.author = originalAuthor
|
||||
it.description = originalDescription
|
||||
it.genre = originalGenre
|
||||
it.status = originalStatus
|
||||
// SY <--
|
||||
it.thumbnail_url = thumbnail_url
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN = 0
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
const val PUBLISHING_FINISHED = 4
|
||||
const val CANCELLED = 5
|
||||
const val ON_HIATUS = 6
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
operator fun invoke(
|
||||
url: String,
|
||||
title: String,
|
||||
artist: String? = null,
|
||||
author: String? = null,
|
||||
description: String? = null,
|
||||
genre: String? = null,
|
||||
status: Int = 0,
|
||||
thumbnail_url: String? = null,
|
||||
initialized: Boolean = false,
|
||||
): SManga {
|
||||
return create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre
|
||||
it.status = status
|
||||
it.thumbnail_url = thumbnail_url
|
||||
it.initialized = initialized
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun SManga.copy(
|
||||
url: String = this.url,
|
||||
title: String = this.originalTitle,
|
||||
artist: String? = this.originalArtist,
|
||||
author: String? = this.originalAuthor,
|
||||
description: String? = this.originalDescription,
|
||||
genre: String? = this.originalGenre,
|
||||
status: Int = this.status,
|
||||
thumbnail_url: String? = this.thumbnail_url,
|
||||
initialized: Boolean = this.initialized,
|
||||
) = SManga.create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre
|
||||
it.status = status
|
||||
it.thumbnail_url = thumbnail_url
|
||||
it.initialized = initialized
|
||||
}
|
||||
// SY <--
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import data.Mangas
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
fun SManga.copyFrom(other: Mangas) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank() && originalTitle != other.title) {
|
||||
val oldTitle = originalTitle
|
||||
title = other.title
|
||||
val source = (this as? Manga)?.source
|
||||
if (source != null) {
|
||||
Injekt.get<DownloadManager>().renameMangaDir(oldTitle, other.title, source)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre.joinToString(separator = ", ")
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
status = other.status.toInt()
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
// SY -->
|
||||
override var title: String = ""
|
||||
// SY <--
|
||||
|
||||
override var artist: String? = null
|
||||
|
||||
override var author: String? = null
|
||||
|
||||
override var description: String? = null
|
||||
|
||||
override var genre: String? = null
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var thumbnail_url: String? = null
|
||||
|
||||
override var initialized: Boolean = false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
|
||||
interface BrowseSourceFilterHeader : CatalogueSource {
|
||||
fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): RecyclerView.Adapter<*>
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
|
||||
interface FollowsSource : CatalogueSource {
|
||||
suspend fun fetchFollows(page: Int): MangasPage
|
||||
|
||||
/**
|
||||
* Returns a list of all Follows retrieved by Coroutines
|
||||
*
|
||||
* @param SManga all smanga found for user
|
||||
*/
|
||||
suspend fun fetchAllFollows(): List<Pair<SManga, RaisedSearchMetadata>>
|
||||
|
||||
/**
|
||||
* updates the follow status for a manga
|
||||
*/
|
||||
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean
|
||||
|
||||
/**
|
||||
* Get a MdList Track of the manga
|
||||
*/
|
||||
suspend fun fetchTrackingInfo(url: String): Track
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.app.Application
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.AndroidCookieJar
|
||||
import eu.kanade.tachiyomi.network.CACHE_CONTROL_NO_STORE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
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 exh.log.maybeInjectEHLogger
|
||||
import exh.patch.injectPatches
|
||||
import exh.source.DelegatedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
abstract class HttpSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
// SY -->
|
||||
protected val network: NetworkHelper by lazy {
|
||||
val network = Injekt.get<NetworkHelper>()
|
||||
object : NetworkHelper(Injekt.get<Application>()) {
|
||||
override val client: OkHttpClient
|
||||
get() = delegate?.networkHttpClient ?: network.client
|
||||
.newBuilder()
|
||||
.injectPatches { id }
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
override val cloudflareClient: OkHttpClient
|
||||
get() = delegate?.networkCloudflareClient ?: network.cloudflareClient
|
||||
.newBuilder()
|
||||
.injectPatches { id }
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
override val cookieManager: AndroidCookieJar
|
||||
get() = network.cookieManager
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* Version id used to generate the source id. If the site completely changes and urls are
|
||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||
*/
|
||||
open val versionId = 1
|
||||
|
||||
/**
|
||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
* of the MD5 of the string: sourcename/language/versionId
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id by lazy {
|
||||
val key = "${name.lowercase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
/* SY --> */
|
||||
open /* SY <-- */ val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
open val client: OkHttpClient
|
||||
// SY -->
|
||||
get() = delegate?.baseHttpClient ?: network.client
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.uppercase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
protected abstract fun popularMangaRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun popularMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return Observable.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun searchMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
/* SY --> protected <-- SY */
|
||||
abstract fun latestUpdatesRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
/* SY --> protected <-- SY */
|
||||
abstract fun latestUpdatesParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
protected open fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
protected open fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun pageListParse(response: Response): List<Page>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
* override the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun imageUrlParse(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
/* SY --> */
|
||||
open /* SY <-- */ fun fetchImage(page: Page): Observable<Response> {
|
||||
val request = imageRequest(page).newBuilder()
|
||||
// images will be cached or saved manually, so don't take up network cache
|
||||
.cacheControl(CACHE_CONTROL_NO_STORE)
|
||||
.build()
|
||||
return client.newCallWithProgress(request, page)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
fun SManga.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the given string without the scheme and domain.
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
return try {
|
||||
val uri = URI(orig.replace(" ", "%20"))
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList()
|
||||
|
||||
// EXH -->
|
||||
private var delegate: DelegatedHttpSource? = null
|
||||
get() = if (Injekt.get<PreferencesHelper>().delegateSources().get()) {
|
||||
field
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun bindDelegate(delegate: DelegatedHttpSource) {
|
||||
this.delegate = delegate
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
|
||||
fun HttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
}
|
||||
|
||||
fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface LoginSource : Source {
|
||||
val requiresLogin: Boolean
|
||||
|
||||
val twoFactorAuth: AuthSupport
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
fun getUsername(): String
|
||||
|
||||
fun getPassword(): String
|
||||
|
||||
suspend fun login(username: String, password: String, twoFactorCode: String?): Boolean
|
||||
|
||||
suspend fun logout(): Boolean
|
||||
|
||||
enum class AuthSupport {
|
||||
NOT_SUPPORTED,
|
||||
SUPPORTED,
|
||||
REQUIRED,
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.interactor.InsertFlatMetadata
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* LEWD!
|
||||
*/
|
||||
interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
val getManga: GetManga get() = Injekt.get()
|
||||
val insertFlatMetadata: InsertFlatMetadata get() = Injekt.get()
|
||||
val getFlatMetadataById: GetFlatMetadataById get() = Injekt.get()
|
||||
|
||||
/**
|
||||
* The class of the metadata used by this source
|
||||
*/
|
||||
val metaClass: KClass<M>
|
||||
|
||||
/**
|
||||
* Parse the supplied input into the supplied metadata object
|
||||
*/
|
||||
suspend fun parseIntoMetadata(metadata: M, input: I)
|
||||
|
||||
/**
|
||||
* Use reflection to create a new instance of metadata
|
||||
*/
|
||||
private fun newMetaInstance() = metaClass.constructors.find {
|
||||
it.parameters.isEmpty()
|
||||
}?.call()
|
||||
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
|
||||
/**
|
||||
* Parses metadata from the input and then copies it into the manga
|
||||
*
|
||||
* Will also save the metadata to the DB if possible
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use the MangaInfo variant")
|
||||
fun parseToMangaCompletable(manga: SManga, input: I): Completable = runAsObservable {
|
||||
parseToManga(manga, input)
|
||||
}.toCompletable()
|
||||
|
||||
suspend fun parseToManga(manga: SManga, input: I): SManga {
|
||||
val mangaId = manga.id()
|
||||
val metadata = if (mangaId != null) {
|
||||
val flatMetadata = getFlatMetadataById.await(mangaId)
|
||||
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
||||
} else {
|
||||
newMetaInstance()
|
||||
}
|
||||
|
||||
parseIntoMetadata(metadata, input)
|
||||
if (mangaId != null) {
|
||||
metadata.mangaId = mangaId
|
||||
insertFlatMetadata.await(metadata)
|
||||
}
|
||||
|
||||
return metadata.createMangaInfo(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||
* producer and parses the metadata from the input
|
||||
*
|
||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||
* also be saved to the DB.
|
||||
*/
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("use fetchOrLoadMetadata made for MangaInfo")
|
||||
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> =
|
||||
runAsObservable {
|
||||
fetchOrLoadMetadata(mangaId) { inputProducer().toObservable().awaitSingle() }
|
||||
}.toSingle()
|
||||
|
||||
/**
|
||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||
* producer and parses the metadata from the input
|
||||
*
|
||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||
* also be saved to the DB.
|
||||
*/
|
||||
suspend fun fetchOrLoadMetadata(mangaId: Long?, inputProducer: suspend () -> I): M {
|
||||
val meta = if (mangaId != null) {
|
||||
val flatMetadata = getFlatMetadataById.await(mangaId)
|
||||
flatMetadata?.raise(metaClass)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return meta ?: inputProducer().let { input ->
|
||||
val newMeta = newMetaInstance()
|
||||
parseIntoMetadata(newMeta, input)
|
||||
if (mangaId != null) {
|
||||
newMeta.mangaId = mangaId
|
||||
insertFlatMetadata.await(newMeta)
|
||||
}
|
||||
newMeta
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit)
|
||||
|
||||
suspend fun SManga.id() = getManga.await(url, id)?.id
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface NamespaceSource : Source
|
||||
@@ -1,200 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
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.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
abstract class ParsedHttpSource : HttpSource() {
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
protected abstract fun popularMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [popularMangaSelector].
|
||||
*/
|
||||
protected abstract fun popularMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
protected abstract fun popularMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
protected abstract fun searchMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [searchMangaSelector].
|
||||
*/
|
||||
protected abstract fun searchMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
protected abstract fun searchMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
protected abstract fun latestUpdatesSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [latestUpdatesSelector].
|
||||
*/
|
||||
protected abstract fun latestUpdatesFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
protected abstract fun latestUpdatesNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the details of the manga from the given [document].
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
protected abstract fun mangaDetailsParse(document: Document): SManga
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||
*/
|
||||
protected abstract fun chapterListSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
*/
|
||||
protected abstract fun chapterFromElement(element: Element): SChapter
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the page list.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
protected abstract fun pageListParse(document: Document): List<Page>
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url to the source image from the document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
protected abstract fun imageUrlParse(document: Document): String
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface RandomMangaSource : Source {
|
||||
suspend fun fetchRandomMangaUrl(): String
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
interface UrlImportableSource : Source {
|
||||
val matchingHosts: List<String>
|
||||
|
||||
fun matchesUri(uri: Uri): Boolean {
|
||||
return uri.host.orEmpty().lowercase() in matchingHosts
|
||||
}
|
||||
|
||||
fun mapUrlToChapterUrl(uri: Uri): String? = null
|
||||
|
||||
suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? = null
|
||||
|
||||
// This method is allowed to block for IO if necessary
|
||||
suspend fun mapUrlToMangaUrl(uri: Uri): String?
|
||||
|
||||
fun cleanMangaUrl(url: String): String {
|
||||
return try {
|
||||
val uri = URI(url)
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanChapterUrl(url: String): String {
|
||||
return try {
|
||||
val uri = URI(url)
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
@@ -28,7 +27,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
@@ -51,7 +49,6 @@ import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUA
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.ui.login.EhLoginActivity
|
||||
import exh.ui.metadata.adapters.EHentaiDescription
|
||||
import exh.util.UriFilter
|
||||
import exh.util.UriGroup
|
||||
import exh.util.asObservableWithAsyncStacktrace
|
||||
@@ -1127,11 +1124,6 @@ class EHentai(
|
||||
return "${uri.scheme}://${uri.host}/g/${obj["gid"]!!.jsonPrimitive.int}/${obj["token"]!!.jsonPrimitive.content}/"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
EHentaiDescription(state, openMetadataViewer, search)
|
||||
}
|
||||
|
||||
override suspend fun getPagePreviewList(
|
||||
manga: SManga,
|
||||
page: Int,
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -11,13 +10,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.HitomiSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HitomiDescription
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -136,11 +133,6 @@ class Hitomi(delegate: HttpSource, val context: Context) :
|
||||
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
HitomiDescription(state, openMetadataViewer)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val otherId = 2703068117101782422L
|
||||
private val DATE_FORMAT by lazy {
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@@ -14,7 +13,6 @@ import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
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.BrowseSourceFilterHeader
|
||||
import eu.kanade.tachiyomi.source.online.FollowsSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
@@ -22,10 +20,7 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.dto.StatisticsMangaDto
|
||||
import exh.md.handlers.ApiMangaParser
|
||||
@@ -49,7 +44,6 @@ import exh.md.utils.MdLang
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.MangaDexDescription
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@@ -64,7 +58,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
UrlImportableSource,
|
||||
FollowsSource,
|
||||
LoginSource,
|
||||
BrowseSourceFilterHeader,
|
||||
RandomMangaSource,
|
||||
NamespaceSource {
|
||||
override val lang: String = delegate.lang
|
||||
@@ -217,11 +210,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
// MetadataSource methods
|
||||
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
MangaDexDescription(state, openMetadataViewer)
|
||||
}
|
||||
|
||||
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) {
|
||||
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third)
|
||||
}
|
||||
@@ -272,11 +260,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return followsHandler.fetchAllFollows()
|
||||
}
|
||||
|
||||
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||
return followsHandler.updateFollowStatus(mangaID, followStatus)
|
||||
}
|
||||
|
||||
override suspend fun fetchTrackingInfo(url: String): Track {
|
||||
suspend fun fetchTrackingInfo(url: String): Track {
|
||||
return followsHandler.fetchTrackingInfo(url)
|
||||
}
|
||||
|
||||
@@ -293,11 +281,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return mangaHandler.getTrackingInfo(track)
|
||||
}
|
||||
|
||||
// BrowseSourceFilterHeader method
|
||||
override fun getFilterHeader(controller: BaseController<*>, onClick: () -> Unit): MangaDexFabHeaderAdapter {
|
||||
return MangaDexFabHeaderAdapter(controller, this, onClick)
|
||||
}
|
||||
|
||||
// RandomMangaSource method
|
||||
override suspend fun fetchRandomMangaUrl(): String {
|
||||
return mangaHandler.fetchRandomMangaId()
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
@@ -16,12 +15,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.NHentaiDescription
|
||||
import exh.util.trimOrNull
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -175,11 +172,6 @@ class NHentai(delegate: HttpSource, val context: Context) :
|
||||
return "$baseUrl/g/${uri.pathSegments[1]}/"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
NHentaiDescription(state, openMetadataViewer)
|
||||
}
|
||||
|
||||
override suspend fun getPagePreviewList(manga: SManga, page: Int): PagePreviewPage {
|
||||
val metadata = fetchOrLoadMetadata(manga.id()) {
|
||||
client.newCall(mangaDetailsRequest(manga)).await()
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -10,13 +9,11 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PervEdenDescription
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -132,9 +129,4 @@ class PervEden(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
return newUri.toString()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
PervEdenDescription(state, openMetadataViewer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -11,12 +10,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.EightMusesDescription
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -96,9 +93,4 @@ class EightMuses(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
return "/comics/album/${path.joinToString("/")}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
EightMusesDescription(state, openMetadataViewer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -10,12 +9,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.HBrowseDescription
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -84,9 +81,4 @@ class HBrowse(delegate: HttpSource, val context: Context) :
|
||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
return uri.pathSegments.firstOrNull()?.let { "/$it/c00001/" }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
HBrowseDescription(state, openMetadataViewer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
@@ -12,13 +11,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.PururinSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.PururinDescription
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
@@ -116,9 +113,4 @@ class Pururin(delegate: HttpSource, val context: Context) :
|
||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String {
|
||||
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments.getOrNull(1)}/${uri.lastPathSegment}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
PururinDescription(state, openMetadataViewer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
@@ -11,14 +10,12 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||
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.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.TsuminoDescription
|
||||
import exh.util.dropBlank
|
||||
import exh.util.trimAll
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
@@ -141,9 +138,4 @@ class Tsumino(delegate: HttpSource, val context: Context) :
|
||||
val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex()
|
||||
val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DescriptionComposable(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
TsuminoDescription(state, openMetadataViewer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.savedsearches.EXHSavedSearch
|
||||
import exh.source.getMainSource
|
||||
|
||||
@@ -117,8 +118,12 @@ class SourceFilterSheet(
|
||||
recycler.adapter = ConcatAdapter(
|
||||
listOfNotNull(
|
||||
controller?.let {
|
||||
source?.getMainSource<BrowseSourceFilterHeader>()
|
||||
?.getFilterHeader(it) { dismissSheet?.invoke() }
|
||||
source?.getMainSource<MangaDex>()
|
||||
?.let {
|
||||
MangaDexFabHeaderAdapter(controller, it) {
|
||||
dismissSheet?.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
savedSearchesAdapter,
|
||||
adapter,
|
||||
|
||||
@@ -46,7 +46,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
||||
// select from auto complete
|
||||
holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ ->
|
||||
val name = adapterView.getItemAtPosition(chipPosition) as String
|
||||
if (name !in if (filter.excludePrefix != null && name.startsWith(filter.excludePrefix)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
|
||||
if (name !in if (filter.excludePrefix != null && name.startsWith(filter.excludePrefix!!)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
|
||||
holder.autoComplete.text = null
|
||||
addTag(name, holder)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
||||
|
||||
// done keyboard button is pressed
|
||||
holder.autoComplete.setOnEditorActionListener { textView, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && textView.text.toString() !in if (filter.excludePrefix != null && textView.text.toString().startsWith(filter.excludePrefix)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && textView.text.toString() !in if (filter.excludePrefix != null && textView.text.toString().startsWith(filter.excludePrefix!!)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
|
||||
textView.text = null
|
||||
addTag(textView.text.toString(), holder)
|
||||
return@setOnEditorActionListener true
|
||||
|
||||
@@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.widget.preference.ThemesPreference
|
||||
import java.util.Date
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
fun Element.selectText(css: String, defaultValue: String? = null): String? {
|
||||
return select(css).first()?.text() ?: defaultValue
|
||||
}
|
||||
|
||||
fun Element.selectInt(css: String, defaultValue: Int = 0): Int {
|
||||
return select(css).first()?.text()?.toInt() ?: defaultValue
|
||||
}
|
||||
|
||||
fun Element.attrOrText(css: String): String {
|
||||
return if (css != "text") attr(css) else text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Jsoup document for this response.
|
||||
* @param html the body of the response. Use only if the body was read before calling this method.
|
||||
*/
|
||||
fun Response.asJsoup(html: String? = null): Document {
|
||||
return Jsoup.parse(html ?: body.string(), request.url.toString())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)
|
||||
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
|
||||
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.IO, block = block)
|
||||
|
||||
fun CoroutineScope.launchNonCancellableIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launchIO { withContext(NonCancellable, block) }
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
@@ -1,87 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import rx.Emitter
|
||||
import rx.Observable
|
||||
import rx.Subscriber
|
||||
import rx.Subscription
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/*
|
||||
* Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
|
||||
*/
|
||||
|
||||
suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
|
||||
|
||||
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
||||
cont.unsubscribeOnCancellation(
|
||||
subscribe(
|
||||
object : Subscriber<T>() {
|
||||
override fun onStart() {
|
||||
request(1)
|
||||
}
|
||||
|
||||
override fun onNext(t: T) {
|
||||
cont.resume(t)
|
||||
}
|
||||
|
||||
override fun onCompleted() {
|
||||
if (cont.isActive) {
|
||||
cont.resumeWithException(
|
||||
IllegalStateException(
|
||||
"Should have invoked onNext",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
/*
|
||||
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
||||
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
||||
* if resume failed, then we know that continuation successfully cancelled itself
|
||||
*/
|
||||
val token = cont.tryResumeWithException(e)
|
||||
if (token != null) {
|
||||
cont.completeResume(token)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
|
||||
invokeOnCancellation { sub.unsubscribe() }
|
||||
|
||||
fun <T> runAsObservable(
|
||||
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
|
||||
block: suspend () -> T,
|
||||
): Observable<T> {
|
||||
return Observable.create(
|
||||
{ emitter ->
|
||||
val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
|
||||
try {
|
||||
emitter.onNext(block())
|
||||
emitter.onCompleted()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
|
||||
if (e !is CancellationException) {
|
||||
emitter.onError(e)
|
||||
} else {
|
||||
emitter.onCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
emitter.setCancellation { job.cancel() }
|
||||
},
|
||||
backpressureMode,
|
||||
)
|
||||
}
|
||||
@@ -24,10 +24,8 @@ import android.util.TypedValue
|
||||
import android.view.Display
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -52,29 +50,6 @@ import kotlin.math.roundToInt
|
||||
|
||||
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
|
||||
|
||||
/**
|
||||
* Display a toast in this context.
|
||||
*
|
||||
* @param resource the text resource.
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
||||
return toast(getString(resource), duration, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a toast in this context.
|
||||
*
|
||||
* @param text the text to display.
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
||||
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
|
||||
block(it)
|
||||
it.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a string to clipboard
|
||||
*
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import logcat.LogPriority
|
||||
|
||||
object DeviceUtil {
|
||||
|
||||
val isMiui by lazy {
|
||||
getSystemProperty("ro.miui.ui.version.name")?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun isMiuiOptimizationDisabled(): Boolean {
|
||||
val sysProp = getSystemProperty("persist.sys.miui_optimization")
|
||||
if (sysProp == "0" || sysProp == "false") {
|
||||
return true
|
||||
}
|
||||
|
||||
return try {
|
||||
Class.forName("android.miui.AppOpsUtils")
|
||||
.getDeclaredMethod("isXOptMode")
|
||||
.invoke(null) as Boolean
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val isSamsung by lazy {
|
||||
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
}
|
||||
|
||||
val isDynamicColorAvailable by lazy {
|
||||
DynamicColors.isDynamicColorAvailable() || (isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
}
|
||||
|
||||
val invalidDefaultBrowsers = listOf("android", "com.huawei.android.internal.app")
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getSystemProperty(key: String?): String? {
|
||||
return try {
|
||||
Class.forName("android.os.SystemProperties")
|
||||
.getDeclaredMethod("get", String::class.java)
|
||||
.invoke(null, key) as String
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.WARN, e) { "Unable to use SystemProperties.get()" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.os.Build
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
val DeviceUtil.isDynamicColorAvailable by lazy {
|
||||
DynamicColors.isDynamicColorAvailable() || (DeviceUtil.isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import logcat.LogPriority
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
||||
inline fun Any.logcat(
|
||||
priority: LogPriority = LogPriority.DEBUG,
|
||||
throwable: Throwable? = null,
|
||||
message: () -> String = { "" },
|
||||
) = logcat(priority = priority) {
|
||||
var msg = message()
|
||||
if (throwable != null) {
|
||||
if (msg.isNotBlank()) msg += "\n"
|
||||
msg += throwable.asLog()
|
||||
}
|
||||
msg
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
abstract class WebViewClientCompat : WebViewClient() {
|
||||
|
||||
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean,
|
||||
) {
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
final override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
): Boolean {
|
||||
return shouldOverrideUrlCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrlCompat(view, url)
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String,
|
||||
): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, url)
|
||||
}
|
||||
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError,
|
||||
) {
|
||||
onReceivedErrorCompat(
|
||||
view,
|
||||
error.errorCode,
|
||||
error.description?.toString(),
|
||||
request.url.toString(),
|
||||
request.isForMainFrame,
|
||||
)
|
||||
}
|
||||
|
||||
final override fun onReceivedError(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
) {
|
||||
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
|
||||
}
|
||||
|
||||
final override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceResponse,
|
||||
) {
|
||||
onReceivedErrorCompat(
|
||||
view,
|
||||
error.statusCode,
|
||||
error.reasonPhrase,
|
||||
request.url
|
||||
.toString(),
|
||||
request.isForMainFrame,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import logcat.LogPriority
|
||||
|
||||
object WebViewUtil {
|
||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||
|
||||
const val MINIMUM_WEBVIEW_VERSION = 100
|
||||
|
||||
fun supportsWebView(context: Context): Boolean {
|
||||
try {
|
||||
// May throw android.webkit.WebViewFactory$MissingWebViewPackageException if WebView
|
||||
// is not installed
|
||||
CookieManager.getInstance()
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return false
|
||||
}
|
||||
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)
|
||||
}
|
||||
}
|
||||
|
||||
fun WebView.isOutdated(): Boolean {
|
||||
return getWebViewMajorVersion() < WebViewUtil.MINIMUM_WEBVIEW_VERSION
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun WebView.setDefaultSettings() {
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = true
|
||||
loadWithOverviewMode = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
private fun WebView.getWebViewMajorVersion(): Int {
|
||||
val uaRegexMatch = """.*Chrome/(\d+)\..*""".toRegex().matchEntire(getDefaultUserAgentString())
|
||||
return if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) {
|
||||
uaRegexMatch.groupValues[1].toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/a/29218966
|
||||
private fun WebView.getDefaultUserAgentString(): String {
|
||||
val originalUA: String = settings.userAgentString
|
||||
|
||||
// Next call to getUserAgentString() will get us the default
|
||||
settings.userAgentString = null
|
||||
val defaultUserAgentString = settings.userAgentString
|
||||
|
||||
// Revert to original UA string
|
||||
settings.userAgentString = originalUA
|
||||
|
||||
return defaultUserAgentString
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package exh.log
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
|
||||
enum class EHLogLevel(@StringRes val nameRes: Int, @StringRes val description: Int) {
|
||||
MINIMAL(R.string.log_minimal, R.string.log_minimal_desc),
|
||||
EXTRA(R.string.log_extra, R.string.log_extra_desc),
|
||||
EXTREME(R.string.log_extreme, R.string.log_extreme_desc),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private var curLogLevel: Int? = null
|
||||
|
||||
val currentLogLevel get() = values()[curLogLevel!!]
|
||||
|
||||
fun init(context: Context) {
|
||||
curLogLevel = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getInt(PreferenceKeys.eh_logLevel, 0)
|
||||
}
|
||||
|
||||
fun shouldLog(requiredLogLevel: EHLogLevel): Boolean {
|
||||
return curLogLevel!! >= requiredLogLevel.ordinal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package exh.log
|
||||
|
||||
import com.elvishew.xlog.XLog
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
fun OkHttpClient.Builder.maybeInjectEHLogger(): OkHttpClient.Builder {
|
||||
if (EHLogLevel.shouldLog(EHLogLevel.EXTREME)) {
|
||||
val logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger { message ->
|
||||
try {
|
||||
Json.decodeFromString<Any>(message)
|
||||
XLog.tag("||EH-NETWORK-JSON").json(message)
|
||||
} catch (ex: Exception) {
|
||||
XLog.tag("||EH-NETWORK").disableBorder().d(message)
|
||||
}
|
||||
}
|
||||
return addInterceptor(HttpLoggingInterceptor(logger).apply { level = HttpLoggingInterceptor.Level.BODY })
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package exh.log
|
||||
|
||||
import android.util.Log
|
||||
import com.elvishew.xlog.Logger
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.elvishew.xlog.LogLevel as XLogLevel
|
||||
|
||||
fun Any.xLog(): Logger = XLog.tag(this::class.java.simpleName).build()
|
||||
|
||||
fun Any.xLogStack(): Logger = XLog.tag(this::class.java.simpleName).enableStackTrace(0).build()
|
||||
|
||||
fun Any.xLogE(log: String) = xLog().e(log)
|
||||
fun Any.xLogW(log: String) = xLog().w(log)
|
||||
fun Any.xLogD(log: String) = xLog().d(log)
|
||||
fun Any.xLogI(log: String) = xLog().i(log)
|
||||
fun Any.xLog(logLevel: LogLevel, log: String) = xLog().log(logLevel.int, log)
|
||||
fun Any.xLogJson(log: String) = xLog().json(log)
|
||||
fun Any.xLogXML(log: String) = xLog().xml(log)
|
||||
|
||||
fun Any.xLogE(log: String, e: Throwable) = xLogStack().e(log, e)
|
||||
fun Any.xLogW(log: String, e: Throwable) = xLogStack().w(log, e)
|
||||
fun Any.xLogD(log: String, e: Throwable) = xLogStack().d(log, e)
|
||||
fun Any.xLogI(log: String, e: Throwable) = xLogStack().i(log, e)
|
||||
fun Any.xLog(logLevel: LogLevel, log: String, e: Throwable) = xLogStack().log(logLevel.int, log, e)
|
||||
|
||||
fun Any.xLogE(log: Any?) = xLog().let { if (log == null) it.e("null") else it.e(log) }
|
||||
fun Any.xLogW(log: Any?) = xLog().let { if (log == null) it.w("null") else it.w(log) }
|
||||
fun Any.xLogD(log: Any?) = xLog().let { if (log == null) it.d("null") else it.d(log) }
|
||||
fun Any.xLogI(log: Any?) = xLog().let { if (log == null) it.i("null") else it.i(log) }
|
||||
fun Any.xLog(logLevel: LogLevel, log: Any?) = xLog().let { if (log == null) it.log(logLevel.int, "null") else it.log(logLevel.int, log) }
|
||||
|
||||
/*fun Any.xLogE(vararg logs: Any) = xLog().e(logs)
|
||||
fun Any.xLogW(vararg logs: Any) = xLog().w(logs)
|
||||
fun Any.xLogD(vararg logs: Any) = xLog().d(logs)
|
||||
fun Any.xLogI(vararg logs: Any) = xLog().i(logs)
|
||||
fun Any.xLog(logLevel: LogLevel, vararg logs: Any) = xLog().log(logLevel.int, logs)*/
|
||||
|
||||
fun Any.xLogE(format: String, vararg args: Any?) = xLog().e(format, *args)
|
||||
fun Any.xLogW(format: String, vararg args: Any?) = xLog().w(format, *args)
|
||||
fun Any.xLogD(format: String, vararg args: Any?) = xLog().d(format, *args)
|
||||
fun Any.xLogI(format: String, vararg args: Any?) = xLog().i(format, *args)
|
||||
fun Any.xLog(logLevel: LogLevel, format: String, vararg args: Any) = xLog().log(logLevel.int, format, *args)
|
||||
|
||||
sealed class LogLevel(val int: Int, val androidLevel: Int) {
|
||||
object None : LogLevel(XLogLevel.NONE, Log.ASSERT)
|
||||
object Error : LogLevel(XLogLevel.ERROR, Log.ERROR)
|
||||
object Warn : LogLevel(XLogLevel.WARN, Log.WARN)
|
||||
object Info : LogLevel(XLogLevel.INFO, Log.INFO)
|
||||
object Debug : LogLevel(XLogLevel.DEBUG, Log.DEBUG)
|
||||
object Verbose : LogLevel(XLogLevel.VERBOSE, Log.VERBOSE)
|
||||
object All : LogLevel(XLogLevel.ALL, Log.VERBOSE)
|
||||
|
||||
val name get() = getLevelName(this)
|
||||
val shortName get() = getLevelShortName(this)
|
||||
|
||||
companion object {
|
||||
fun getLevelName(logLevel: LogLevel): String = XLogLevel.getLevelName(logLevel.int)
|
||||
fun getLevelShortName(logLevel: LogLevel): String = XLogLevel.getShortLevelName(logLevel.int)
|
||||
|
||||
fun values() = listOf(
|
||||
None,
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Verbose,
|
||||
All,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use proper throwable function", ReplaceWith("""xLogE("", log)"""))
|
||||
fun Any.xLogE(log: Throwable) = xLogStack().e(log)
|
||||
|
||||
@Deprecated("Use proper throwable function", ReplaceWith("""xLogW("", log)"""))
|
||||
fun Any.xLogW(log: Throwable) = xLogStack().w(log)
|
||||
|
||||
@Deprecated("Use proper throwable function", ReplaceWith("""xLogD("", log)"""))
|
||||
fun Any.xLogD(log: Throwable) = xLogStack().d(log)
|
||||
|
||||
@Deprecated("Use proper throwable function", ReplaceWith("""xLogI("", log)"""))
|
||||
fun Any.xLogI(log: Throwable) = xLogStack().i(log)
|
||||
|
||||
@Deprecated("Use proper throwable function", ReplaceWith("""xLog(logLevel, "", log)"""))
|
||||
fun Any.xLog(logLevel: LogLevel, log: Throwable) = xLogStack().log(logLevel.int, log)
|
||||
@@ -1,29 +0,0 @@
|
||||
package exh.md.utils
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
enum class MangaDexRelation(@StringRes val resId: Int, val mdString: String?) {
|
||||
SIMILAR(R.string.relation_similar, null),
|
||||
MONOCHROME(R.string.relation_monochrome, "monochrome"),
|
||||
MAIN_STORY(R.string.relation_main_story, "main_story"),
|
||||
ADAPTED_FROM(R.string.relation_adapted_from, "adapted_from"),
|
||||
BASED_ON(R.string.relation_based_on, "based_on"),
|
||||
PREQUEL(R.string.relation_prequel, "prequel"),
|
||||
SIDE_STORY(R.string.relation_side_story, "side_story"),
|
||||
DOUJINSHI(R.string.relation_doujinshi, "doujinshi"),
|
||||
SAME_FRANCHISE(R.string.relation_same_franchise, "same_franchise"),
|
||||
SHARED_UNIVERSE(R.string.relation_shared_universe, "shared_universe"),
|
||||
SEQUEL(R.string.relation_sequel, "sequel"),
|
||||
SPIN_OFF(R.string.relation_spin_off, "spin_off"),
|
||||
ALTERNATE_STORY(R.string.relation_alternate_story, "alternate_story"),
|
||||
PRESERIALIZATION(R.string.relation_preserialization, "preserialization"),
|
||||
COLORED(R.string.relation_colored, "colored"),
|
||||
SERIALIZATION(R.string.relation_serialization, "serialization"),
|
||||
ALTERNATE_VERSION(R.string.relation_alternate_version, "alternate_version"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromDex(mdString: String) = values().find { it.mdString == mdString }
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
@Serializable
|
||||
class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
var gId: String?
|
||||
get() = indexedExtra
|
||||
set(value) { indexedExtra = value }
|
||||
|
||||
var gToken: String? = null
|
||||
var exh: Boolean? = null
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_TITLE)
|
||||
var altTitle by titleDelegate(TITLE_TYPE_ALT_TITLE)
|
||||
|
||||
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 aged: Boolean = false
|
||||
var lastUpdateCheck: Long = 0
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = gId?.let { gId ->
|
||||
gToken?.let { gToken ->
|
||||
idAndTokenToUrl(gId, gToken)
|
||||
}
|
||||
}
|
||||
val cover = thumbnailUrl
|
||||
|
||||
// No title bug?
|
||||
val title = altTitle
|
||||
?.takeIf { Injekt.get<PreferencesHelper>().useJapaneseTitle().get() }
|
||||
?: title
|
||||
|
||||
// Set artist (if we can find one)
|
||||
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE)
|
||||
.ifEmpty { null }
|
||||
?.joinToString { it.name }
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
// We default to completed
|
||||
var status = SManga.COMPLETED
|
||||
title?.let { t ->
|
||||
MetadataUtil.ONGOING_SUFFIX.find {
|
||||
t.endsWith(it, ignoreCase = true)
|
||||
}?.let {
|
||||
status = SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
title = title ?: manga.title,
|
||||
artist = artist ?: manga.artist,
|
||||
description = description,
|
||||
genre = genres,
|
||||
status = status,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(gId) { getString(R.string.id) },
|
||||
getItem(gToken) { getString(R.string.token) },
|
||||
getItem(exh) { getString(R.string.is_exhentai_gallery) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitle) { getString(R.string.alt_title) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(datePosted, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||
getItem(parent) { getString(R.string.parent) },
|
||||
getItem(visible) { getString(R.string.visible) },
|
||||
getItem(language) { getString(R.string.language) },
|
||||
getItem(translated) { getString(R.string.translated) },
|
||||
getItem(size, { MetadataUtil.humanReadableByteCount(it, true) }) { getString(R.string.gallery_size) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
getItem(favorites) { getString(R.string.total_favorites) },
|
||||
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
getItem(aged) { getString(R.string.aged) },
|
||||
getItem(lastUpdateCheck, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.last_update_check) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_TITLE = 0
|
||||
private const val TITLE_TYPE_ALT_TITLE = 1
|
||||
|
||||
const val TAG_TYPE_NORMAL = 0
|
||||
const val TAG_TYPE_LIGHT = 1
|
||||
const val TAG_TYPE_WEAK = 2
|
||||
|
||||
const val EH_GENRE_NAMESPACE = "genre"
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
const val EH_LANGUAGE_NAMESPACE = "language"
|
||||
const val EH_META_NAMESPACE = "meta"
|
||||
const val EH_UPLOADER_NAMESPACE = "uploader"
|
||||
const val EH_VISIBILITY_NAMESPACE = "visibility"
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http")) {
|
||||
it.toUri().pathSegments
|
||||
} else {
|
||||
it.split('/')
|
||||
}
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
fun galleryId(url: String): String = splitGalleryUrl(url)[1]
|
||||
|
||||
fun galleryToken(url: String): String =
|
||||
splitGalleryUrl(url)[2]
|
||||
|
||||
fun normalizeUrl(url: String) =
|
||||
idAndTokenToUrl(galleryId(url), galleryToken(url))
|
||||
|
||||
fun idAndTokenToUrl(id: String, token: String) =
|
||||
"/g/$id/$token/?nw=always"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.nullIfEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
var path: List<String> = emptyList()
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_MAIN)
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = path.joinToString("/", prefix = "/")
|
||||
|
||||
val title = title
|
||||
|
||||
val cover = thumbnailUrl
|
||||
|
||||
val artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name }
|
||||
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key,
|
||||
title = title ?: manga.title,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
artist = artist,
|
||||
genre = genres,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(path.nullIfEmpty(), { it.joinToString("/", prefix = "/") }) { getString(R.string.path) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
const val TAGS_NAMESPACE = "tags"
|
||||
const val ARTIST_NAMESPACE = "artist"
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
var hbId: Long? = null
|
||||
|
||||
var hbUrl: String? = null
|
||||
|
||||
var thumbnail: String? = null
|
||||
|
||||
var title: String? by titleDelegate(TITLE_TYPE_MAIN)
|
||||
|
||||
// Length in pages
|
||||
var length: Int? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = hbUrl
|
||||
|
||||
val title = title
|
||||
|
||||
// Guess thumbnail URL if manga does not have thumbnail URL
|
||||
val cover = if (manga.thumbnail_url.isNullOrBlank()) {
|
||||
guessThumbnailUrl(hbId.toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name }
|
||||
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
title = title ?: manga.title,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
artist = artist,
|
||||
genre = genres,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(hbId) { getString(R.string.id) },
|
||||
getItem(hbUrl) { getString(R.string.url) },
|
||||
getItem(thumbnail) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://www.hbrowse.com"
|
||||
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
const val ARTIST_NAMESPACE = "artist"
|
||||
|
||||
fun guessThumbnailUrl(hbid: String): String {
|
||||
return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.nullIfEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Date
|
||||
|
||||
@Serializable
|
||||
class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
var url get() = hlId?.let { urlFromHlId(it) }
|
||||
set(a) {
|
||||
a?.let {
|
||||
hlId = hlIdFromUrl(a)
|
||||
}
|
||||
}
|
||||
|
||||
var hlId: String? = null
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_MAIN)
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var artists: List<String> = emptyList()
|
||||
|
||||
var genre: String? = null
|
||||
|
||||
var language: String? = null
|
||||
|
||||
var uploadDate: Long? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val cover = thumbnailUrl
|
||||
|
||||
val title = title
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val artist = artists.joinToString()
|
||||
|
||||
val status = SManga.UNKNOWN
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
title = title ?: manga.title,
|
||||
genre = genres,
|
||||
artist = artist,
|
||||
status = status,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(hlId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(artists.nullIfEmpty(), { it.joinToString() }) { getString(R.string.artist) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(language) { getString(R.string.language) },
|
||||
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
const val BASE_URL = "https://hitomi.la"
|
||||
|
||||
fun hlIdFromUrl(url: String) =
|
||||
url.split('/').last().split('-').last().substringBeforeLast('.')
|
||||
|
||||
fun urlFromHlId(id: String) =
|
||||
"$BASE_URL/galleries/$id.html"
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.md.utils.MangaDexRelation
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||
var mdUuid: String? = null
|
||||
|
||||
// var mdUrl: String? = null
|
||||
|
||||
var cover: String? = null
|
||||
|
||||
var title: String? by titleDelegate(TITLE_TYPE_MAIN)
|
||||
var altTitles: List<String>? = null
|
||||
|
||||
var description: String? = null
|
||||
|
||||
var authors: List<String>? = null
|
||||
var artists: List<String>? = null
|
||||
|
||||
var langFlag: String? = null
|
||||
|
||||
var lastChapterNumber: Int? = null
|
||||
var rating: Float? = null
|
||||
// var users: String? = null
|
||||
|
||||
var anilistId: String? = null
|
||||
var kitsuId: String? = null
|
||||
var myAnimeListId: String? = null
|
||||
var mangaUpdatesId: String? = null
|
||||
var animePlanetId: String? = null
|
||||
|
||||
var status: Int? = null
|
||||
|
||||
// var missing_chapters: String? = null
|
||||
|
||||
var followStatus: Int? = null
|
||||
var relation: MangaDexRelation? = null
|
||||
|
||||
// var maxChapterNumber: Int? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = mdUuid?.let { MdUtil.buildMangaUrl(it) }
|
||||
|
||||
val title = title
|
||||
|
||||
val cover = cover
|
||||
|
||||
val author = authors?.joinToString()?.let { MdUtil.cleanString(it) }
|
||||
|
||||
val artist = artists?.joinToString()?.let { MdUtil.cleanString(it) }
|
||||
|
||||
val status = status
|
||||
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = description
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
title = title ?: manga.title,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
author = author ?: manga.author,
|
||||
artist = artist ?: manga.artist,
|
||||
status = status ?: manga.status,
|
||||
genre = genres,
|
||||
description = description ?: manga.description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(mdUuid) { getString(R.string.id) },
|
||||
// getItem(mdUrl) { getString(R.string.url) },
|
||||
getItem(cover) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(authors, { it.joinToString() }) { getString(R.string.author) },
|
||||
getItem(artists, { it.joinToString() }) { getString(R.string.artist) },
|
||||
getItem(langFlag) { getString(R.string.language) },
|
||||
getItem(lastChapterNumber) { getString(R.string.last_chapter_number) },
|
||||
getItem(rating) { getString(R.string.average_rating) },
|
||||
// getItem(users) { getString(R.string.total_ratings) },
|
||||
getItem(status) { getString(R.string.status) },
|
||||
// getItem(missing_chapters) { getString(R.string.missing_chapters) },
|
||||
getItem(followStatus) { getString(R.string.follow_status) },
|
||||
getItem(anilistId) { getString(R.string.anilist_id) },
|
||||
getItem(kitsuId) { getString(R.string.kitsu_id) },
|
||||
getItem(myAnimeListId) { getString(R.string.mal_id) },
|
||||
getItem(mangaUpdatesId) { getString(R.string.manga_updates_id) },
|
||||
getItem(animePlanetId) { getString(R.string.anime_planet_id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Date
|
||||
|
||||
@Serializable
|
||||
class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
var url get() = nhId?.let { BASE_URL + nhIdToPath(it) }
|
||||
set(a) {
|
||||
a?.let {
|
||||
nhId = nhUrlToId(a)
|
||||
}
|
||||
}
|
||||
|
||||
var nhId: Long? = null
|
||||
|
||||
var uploadDate: Long? = null
|
||||
|
||||
var favoritesCount: Long? = null
|
||||
|
||||
var mediaId: String? = null
|
||||
|
||||
var japaneseTitle by titleDelegate(TITLE_TYPE_JAPANESE)
|
||||
var englishTitle by titleDelegate(TITLE_TYPE_ENGLISH)
|
||||
var shortTitle by titleDelegate(TITLE_TYPE_SHORT)
|
||||
|
||||
var coverImageType: String? = null
|
||||
var pageImageTypes: List<String> = emptyList()
|
||||
var thumbnailImageType: String? = null
|
||||
|
||||
var scanlator: String? = null
|
||||
|
||||
var preferredTitle: Int? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = nhId?.let { nhIdToPath(it) }
|
||||
|
||||
val cover = if (mediaId != null) {
|
||||
typeToExtension(coverImageType)?.let {
|
||||
"https://t.nhentai.net/galleries/$mediaId/cover.$it"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val title = when (preferredTitle) {
|
||||
TITLE_TYPE_SHORT -> shortTitle ?: englishTitle ?: japaneseTitle ?: manga.title
|
||||
0, TITLE_TYPE_ENGLISH -> englishTitle ?: japaneseTitle ?: shortTitle ?: manga.title
|
||||
else -> englishTitle ?: japaneseTitle ?: shortTitle ?: manga.title
|
||||
}
|
||||
|
||||
// Set artist (if we can find one)
|
||||
val artist = tags.ofNamespace(NHENTAI_ARTIST_NAMESPACE).let { tags ->
|
||||
if (tags.isNotEmpty()) tags.joinToString(transform = { it.name }) else null
|
||||
}
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
// We default to completed
|
||||
var status = SManga.COMPLETED
|
||||
englishTitle?.let { t ->
|
||||
MetadataUtil.ONGOING_SUFFIX.find {
|
||||
t.endsWith(it, ignoreCase = true)
|
||||
}?.let {
|
||||
status = SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
title = title,
|
||||
artist = artist ?: manga.artist,
|
||||
genre = genres,
|
||||
status = status,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(nhId) { getString(R.string.id) },
|
||||
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) }) { getString(R.string.date_posted) },
|
||||
getItem(favoritesCount) { getString(R.string.total_favorites) },
|
||||
getItem(mediaId) { getString(R.string.media_id) },
|
||||
getItem(japaneseTitle) { getString(R.string.japanese_title) },
|
||||
getItem(englishTitle) { getString(R.string.english_title) },
|
||||
getItem(shortTitle) { getString(R.string.short_title) },
|
||||
getItem(coverImageType) { getString(R.string.cover_image_file_type) },
|
||||
getItem(pageImageTypes.size) { getString(R.string.page_count) },
|
||||
getItem(thumbnailImageType) { getString(R.string.thumbnail_image_file_type) },
|
||||
getItem(scanlator) { getString(R.string.scanlator) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_JAPANESE = 0
|
||||
const val TITLE_TYPE_ENGLISH = 1
|
||||
const val TITLE_TYPE_SHORT = 2
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
const val BASE_URL = "https://nhentai.net"
|
||||
|
||||
private const val NHENTAI_ARTIST_NAMESPACE = "artist"
|
||||
const val NHENTAI_CATEGORIES_NAMESPACE = "category"
|
||||
|
||||
fun typeToExtension(t: String?) =
|
||||
when (t) {
|
||||
"p" -> "png"
|
||||
"j" -> "jpg"
|
||||
"g" -> "gif"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun nhUrlToId(url: String) =
|
||||
url.split("/").last { it.isNotBlank() }.toLong()
|
||||
|
||||
fun nhIdToPath(id: Long) = "/g/$id/"
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTitle
|
||||
import exh.util.nullIfEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
var pvId: String? = null
|
||||
|
||||
var url: String? = null
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_MAIN)
|
||||
var altTitles
|
||||
get() = titles.filter { it.type == TITLE_TYPE_ALT }.map { it.title }
|
||||
set(value) {
|
||||
titles.removeAll { it.type == TITLE_TYPE_ALT }
|
||||
titles += value.map { RaisedTitle(it, TITLE_TYPE_ALT) }
|
||||
}
|
||||
|
||||
var artist: String? = null
|
||||
|
||||
var genre: String? = null
|
||||
|
||||
var rating: Float? = null
|
||||
|
||||
var status: String? = null
|
||||
|
||||
var lang: String? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = url
|
||||
val cover = thumbnailUrl
|
||||
|
||||
val title = title
|
||||
|
||||
val artist = artist
|
||||
|
||||
val status = when (status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed", "Suspended" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
title = title ?: manga.title,
|
||||
artist = artist ?: manga.artist,
|
||||
status = status,
|
||||
genre = genres,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(pvId) { getString(R.string.id) },
|
||||
getItem(url) { getString(R.string.url) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitles.nullIfEmpty(), { it.joinToString() }) { getString(R.string.alt_titles) },
|
||||
getItem(artist) { getString(R.string.artist) },
|
||||
getItem(genre) { getString(R.string.genre) },
|
||||
getItem(rating) { getString(R.string.average_rating) },
|
||||
getItem(status) { getString(R.string.status) },
|
||||
getItem(lang) { getString(R.string.language) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
private const val TITLE_TYPE_ALT = 1
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
private fun splitGalleryUrl(url: String) =
|
||||
url.toUri().pathSegments.filterNot(String::isNullOrBlank)
|
||||
|
||||
fun pvIdFromUrl(url: String): String = splitGalleryUrl(url).last()
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
var prId: Int? = null
|
||||
|
||||
var prShortLink: String? = null
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_TITLE)
|
||||
var altTitle by titleDelegate(TITLE_TYPE_ALT_TITLE)
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var uploaderDisp: String? = null
|
||||
|
||||
var pages: Int? = null
|
||||
|
||||
var fileSize: String? = null
|
||||
|
||||
var ratingCount: Int? = null
|
||||
var averageRating: Double? = null
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val key = prId?.let { prId ->
|
||||
prShortLink?.let { prShortLink ->
|
||||
"/gallery/$prId/$prShortLink"
|
||||
}
|
||||
}
|
||||
|
||||
val title = title ?: altTitle
|
||||
|
||||
val cover = thumbnailUrl
|
||||
|
||||
val artist = tags.ofNamespace(TAG_NAMESPACE_ARTIST).joinToString { it.name }
|
||||
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
url = key ?: manga.url,
|
||||
title = title ?: manga.title,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
artist = artist,
|
||||
genre = genres,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(prId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(altTitle) { getString(R.string.alt_title) },
|
||||
getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
|
||||
getItem(uploaderDisp) { getString(R.string.uploader_capital) },
|
||||
getItem(uploader) { getString(R.string.uploader) },
|
||||
getItem(pages) { getString(R.string.page_count) },
|
||||
getItem(fileSize) { getString(R.string.gallery_size) },
|
||||
getItem(ratingCount) { getString(R.string.total_ratings) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_TITLE = 0
|
||||
private const val TITLE_TYPE_ALT_TITLE = 1
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
private const val TAG_NAMESPACE_ARTIST = "artist"
|
||||
const val TAG_NAMESPACE_CATEGORY = "category"
|
||||
|
||||
const val BASE_URL = "https://pururin.io"
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.nullIfEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
var tmId: Int? = null
|
||||
|
||||
var title by titleDelegate(TITLE_TYPE_MAIN)
|
||||
|
||||
var artist: String? = null
|
||||
|
||||
var uploadDate: Long? = null
|
||||
|
||||
var length: Int? = null
|
||||
|
||||
var ratingString: String? = null
|
||||
|
||||
var averageRating: Float? = null
|
||||
|
||||
var userRatings: Long? = null
|
||||
|
||||
var favorites: Long? = null
|
||||
|
||||
var category: String? = null
|
||||
|
||||
var collection: String? = null
|
||||
|
||||
var group: String? = null
|
||||
|
||||
var parody: List<String> = emptyList()
|
||||
|
||||
var character: List<String> = emptyList()
|
||||
|
||||
override fun createMangaInfo(manga: SManga): SManga {
|
||||
val title = title
|
||||
val cover = tmId?.let { BASE_URL.replace("www", "content") + thumbUrlFromId(it.toString()) }
|
||||
|
||||
val artist = artist
|
||||
|
||||
val status = SManga.UNKNOWN
|
||||
|
||||
// Copy tags -> genres
|
||||
val genres = tagsToGenreString()
|
||||
|
||||
val description = "meta"
|
||||
|
||||
return manga.copy(
|
||||
title = title ?: manga.title,
|
||||
thumbnail_url = cover ?: manga.thumbnail_url,
|
||||
artist = artist ?: manga.artist,
|
||||
status = status,
|
||||
genre = genres,
|
||||
description = description,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
return with(context) {
|
||||
listOfNotNull(
|
||||
getItem(tmId) { getString(R.string.id) },
|
||||
getItem(title) { getString(R.string.title) },
|
||||
getItem(uploader) { getString(R.string.uploader) },
|
||||
getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
|
||||
getItem(length) { getString(R.string.page_count) },
|
||||
getItem(ratingString) { getString(R.string.rating_string) },
|
||||
getItem(averageRating) { getString(R.string.average_rating) },
|
||||
getItem(userRatings) { getString(R.string.total_ratings) },
|
||||
getItem(favorites) { getString(R.string.total_favorites) },
|
||||
getItem(category) { getString(R.string.genre) },
|
||||
getItem(collection) { getString(R.string.collection) },
|
||||
getItem(group) { getString(R.string.group) },
|
||||
getItem(parody.nullIfEmpty(), { it.joinToString() }) { getString(R.string.parodies) },
|
||||
getItem(character.nullIfEmpty(), { it.joinToString() }) { getString(R.string.characters) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
|
||||
val BASE_URL = "https://www.tsumino.com"
|
||||
|
||||
val TSUMINO_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
|
||||
fun tmIdFromUrl(url: String) = url.toUri().lastPathSegment
|
||||
|
||||
fun thumbUrlFromId(id: String) = "/thumbs/$id/1"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import exh.metadata.sql.models.SearchMetadata
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Serializable
|
||||
data class FlatMetadata(
|
||||
val metadata: SearchMetadata,
|
||||
val tags: List<SearchTag>,
|
||||
val titles: List<SearchTitle>,
|
||||
) {
|
||||
inline fun <reified T : RaisedSearchMetadata> raise(): T = raise(T::class)
|
||||
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
fun <T : RaisedSearchMetadata> raise(clazz: KClass<T>): T =
|
||||
RaisedSearchMetadata.raiseFlattenJson
|
||||
.decodeFromString(clazz.serializer(), metadata.extra).apply {
|
||||
fillBaseFields(this@FlatMetadata)
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||
import exh.metadata.metadata.HitomiSearchMetadata
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.metadata.metadata.PururinSearchMetadata
|
||||
import exh.metadata.metadata.TsuminoSearchMetadata
|
||||
import exh.metadata.sql.models.SearchMetadata
|
||||
import exh.metadata.sql.models.SearchTag
|
||||
import exh.metadata.sql.models.SearchTitle
|
||||
import exh.util.plusAssign
|
||||
import kotlinx.serialization.Polymorphic
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Polymorphic
|
||||
@Serializable
|
||||
abstract class RaisedSearchMetadata {
|
||||
@Transient
|
||||
var mangaId: Long = -1
|
||||
|
||||
@Transient
|
||||
var uploader: String? = null
|
||||
|
||||
@Transient
|
||||
protected open var indexedExtra: String? = null
|
||||
|
||||
@Transient
|
||||
val tags = mutableListOf<RaisedTag>()
|
||||
|
||||
@Transient
|
||||
val titles = mutableListOf<RaisedTitle>()
|
||||
|
||||
fun getTitleOfType(type: Int): String? = titles.find { it.type == type }?.title
|
||||
|
||||
fun replaceTitleOfType(type: Int, newTitle: String?) {
|
||||
titles.removeAll { it.type == type }
|
||||
if (newTitle != null) titles += RaisedTitle(newTitle, type)
|
||||
}
|
||||
|
||||
fun <T : Any> getItem(
|
||||
item: T?,
|
||||
toString: (T) -> String = Any::toString,
|
||||
block: (T) -> String,
|
||||
): Pair<String, String>? {
|
||||
item ?: return null
|
||||
return block(item) to toString(item)
|
||||
}
|
||||
|
||||
open fun copyTo(manga: SManga) {
|
||||
val infoManga = createMangaInfo(manga.copy())
|
||||
manga.copyFrom(infoManga)
|
||||
}
|
||||
|
||||
abstract fun createMangaInfo(manga: SManga): SManga
|
||||
|
||||
fun tagsToGenreString() = tags.toGenreString()
|
||||
|
||||
fun tagsToGenreList() = tags.toGenreList()
|
||||
|
||||
fun tagsToDescription() =
|
||||
StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
|
||||
val groupedTags = tags.filter { it.type != TAG_TYPE_VIRTUAL }.groupBy {
|
||||
it.namespace
|
||||
}.entries
|
||||
|
||||
groupedTags.forEach { (namespace, tags) ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
if (namespace != null) {
|
||||
this += "▪ "
|
||||
this += namespace
|
||||
this += ": "
|
||||
}
|
||||
this += joinedTags
|
||||
this += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun List<RaisedTag>.ofNamespace(ns: String): List<RaisedTag> {
|
||||
return filter { it.namespace == ns }
|
||||
}
|
||||
|
||||
fun flatten(): FlatMetadata {
|
||||
require(mangaId != -1L)
|
||||
|
||||
val extra = raiseFlattenJson.encodeToString(this)
|
||||
return FlatMetadata(
|
||||
SearchMetadata(
|
||||
mangaId,
|
||||
uploader,
|
||||
extra,
|
||||
indexedExtra,
|
||||
0,
|
||||
),
|
||||
tags.map {
|
||||
SearchTag(
|
||||
null,
|
||||
mangaId,
|
||||
it.namespace,
|
||||
it.name,
|
||||
it.type,
|
||||
)
|
||||
},
|
||||
titles.map {
|
||||
SearchTitle(
|
||||
null,
|
||||
mangaId,
|
||||
it.title,
|
||||
it.type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun fillBaseFields(metadata: FlatMetadata) {
|
||||
mangaId = metadata.metadata.mangaId
|
||||
uploader = metadata.metadata.uploader
|
||||
indexedExtra = metadata.metadata.indexedExtra
|
||||
|
||||
this.tags.clear()
|
||||
this.tags += metadata.tags.map {
|
||||
RaisedTag(it.namespace, it.name, it.type)
|
||||
}
|
||||
|
||||
this.titles.clear()
|
||||
this.titles += metadata.titles.map {
|
||||
RaisedTitle(it.title, it.type)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getExtraInfoPairs(context: Context): List<Pair<String, String>>
|
||||
|
||||
companion object {
|
||||
// Virtual tags allow searching of otherwise unindexed fields
|
||||
const val TAG_TYPE_VIRTUAL = -2
|
||||
|
||||
fun MutableList<RaisedTag>.toGenreString() =
|
||||
this.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.joinToString { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
|
||||
fun MutableList<RaisedTag>.toGenreList() =
|
||||
this.filter { it.type != TAG_TYPE_VIRTUAL }
|
||||
.map { (if (it.namespace != null) "${it.namespace}: " else "") + it.name }
|
||||
|
||||
private val module = SerializersModule {
|
||||
polymorphic(RaisedSearchMetadata::class) {
|
||||
subclass(EHentaiSearchMetadata::class)
|
||||
subclass(EightMusesSearchMetadata::class)
|
||||
subclass(HBrowseSearchMetadata::class)
|
||||
subclass(HitomiSearchMetadata::class)
|
||||
subclass(MangaDexSearchMetadata::class)
|
||||
subclass(NHentaiSearchMetadata::class)
|
||||
subclass(PervEdenSearchMetadata::class)
|
||||
subclass(PururinSearchMetadata::class)
|
||||
subclass(TsuminoSearchMetadata::class)
|
||||
}
|
||||
}
|
||||
|
||||
val raiseFlattenJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule = module
|
||||
}
|
||||
|
||||
fun titleDelegate(type: Int) = object : ReadWriteProperty<RaisedSearchMetadata, String?> {
|
||||
/**
|
||||
* Returns the value of the property for the given object.
|
||||
* @param thisRef the object for which the value is requested.
|
||||
* @param property the metadata for the property.
|
||||
* @return the property value.
|
||||
*/
|
||||
override fun getValue(thisRef: RaisedSearchMetadata, property: KProperty<*>) =
|
||||
thisRef.getTitleOfType(type)
|
||||
|
||||
/**
|
||||
* Sets the value of the property for the given object.
|
||||
* @param thisRef the object for which the value is requested.
|
||||
* @param property the metadata for the property.
|
||||
* @param value the value to set.
|
||||
*/
|
||||
override fun setValue(thisRef: RaisedSearchMetadata, property: KProperty<*>, value: String?) =
|
||||
thisRef.replaceTitleOfType(type, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RaisedTag(
|
||||
val namespace: String?,
|
||||
val name: String,
|
||||
val type: Int,
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RaisedTitle(
|
||||
val title: String,
|
||||
val type: Int = 0,
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchMetadata(
|
||||
// Manga ID this gallery is linked to
|
||||
val mangaId: Long,
|
||||
|
||||
// Gallery uploader
|
||||
val uploader: String?,
|
||||
|
||||
// Extra data attached to this metadata, in JSON format
|
||||
val extra: String,
|
||||
|
||||
// Indexed extra data attached to this metadata
|
||||
val indexedExtra: String?,
|
||||
|
||||
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema
|
||||
val extraVersion: Int,
|
||||
) {
|
||||
// Transient information attached to this piece of metadata, useful for caching
|
||||
|
||||
var transientCache: Map<String, @Contextual Any>? = null
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchTag(
|
||||
// Tag identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this tag is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Tag namespace
|
||||
val namespace: String?,
|
||||
|
||||
// Tag name
|
||||
val name: String,
|
||||
|
||||
// Tag type
|
||||
val type: Int,
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
package exh.metadata.sql.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchTitle(
|
||||
// Title identifier, unique
|
||||
val id: Long?,
|
||||
|
||||
// Metadata this title is attached to
|
||||
val mangaId: Long,
|
||||
|
||||
// Title
|
||||
val title: String,
|
||||
|
||||
// Title type, useful for distinguishing between main/alt titles
|
||||
val type: Int,
|
||||
)
|
||||
@@ -1,297 +0,0 @@
|
||||
package exh.source
|
||||
|
||||
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.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
abstract class DelegatedHttpSource(val delegate: HttpSource) : HttpSource() {
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl get() = delegate.baseUrl
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
override val headers get() = delegate.headers
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest get() = delegate.supportsLatest
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
final override val name get() = delegate.name
|
||||
|
||||
// ===> OPTIONAL FIELDS
|
||||
|
||||
/**
|
||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
* of the MD5 of the string: sourcename/language/versionId
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id get() = delegate.id
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
final override val client get() = delegate.client
|
||||
|
||||
/**
|
||||
* You must NEVER call super.client if you override this!
|
||||
*/
|
||||
open val baseHttpClient: OkHttpClient? = null
|
||||
open val networkHttpClient: OkHttpClient get() = network.client
|
||||
open val networkCloudflareClient: OkHttpClient get() = network.cloudflareClient
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = delegate.toString()
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchPopularManga(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchLatestUpdates(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchMangaDetails(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get the updated details for a manga.
|
||||
*/
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.getMangaDetails(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.mangaDetailsRequest(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchChapterList(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get all the available chapters for a manga.
|
||||
*/
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.getChapterList(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchPageList(chapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get the list of pages a chapter has.
|
||||
*/
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.getPageList(chapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchImageUrl(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.fetchImage(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
ensureDelegateCompatible()
|
||||
return delegate.prepareNewChapter(chapter, manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = delegate.getFilterList()
|
||||
|
||||
protected open fun ensureDelegateCompatible() {
|
||||
if (versionId != delegate.versionId || lang != delegate.lang) {
|
||||
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId}, lang: $lang <=> ${delegate.lang})!")
|
||||
}
|
||||
}
|
||||
|
||||
class IncompatibleDelegateException(message: String) : RuntimeException(message)
|
||||
|
||||
init {
|
||||
delegate.bindDelegate(this)
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package exh.source
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
class EnhancedHttpSource(
|
||||
val originalSource: HttpSource,
|
||||
val enhancedSource: HttpSource,
|
||||
) : HttpSource() {
|
||||
private val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("Should never be called!")
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
override val baseUrl get() = source().baseUrl
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
override val headers get() = source().headers
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
override val supportsLatest get() = source().supportsLatest
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
override val name get() = source().name
|
||||
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
override val lang get() = source().lang
|
||||
|
||||
// ===> OPTIONAL FIELDS
|
||||
|
||||
/**
|
||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
* of the MD5 of the string: sourcename/language/versionId
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id get() = source().id
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
override val client get() = originalSource.client // source().client
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = source().toString()
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchPopularManga(page: Int) = source().fetchPopularManga(page)
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||
source().fetchSearchManga(page, query, filters)
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchLatestUpdates(page: Int) = source().fetchLatestUpdates(page)
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga) = source().fetchMangaDetails(manga)
|
||||
|
||||
/**
|
||||
* [1.x API] Get the updated details for a manga.
|
||||
*/
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = source().getMangaDetails(manga)
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun mangaDetailsRequest(manga: SManga) = source().mangaDetailsRequest(manga)
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga) = source().fetchChapterList(manga)
|
||||
|
||||
/**
|
||||
* [1.x API] Get all the available chapters for a manga.
|
||||
*/
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> = source().getChapterList(manga)
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter) = source().fetchPageList(chapter)
|
||||
|
||||
/**
|
||||
* [1.x API] Get the list of pages a chapter has.
|
||||
*/
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = source().getPageList(chapter)
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
override fun fetchImageUrl(page: Page) = source().fetchImageUrl(page)
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
override fun fetchImage(page: Page) = source().fetchImage(page)
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) =
|
||||
source().prepareNewChapter(chapter, manga)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = source().getFilterList()
|
||||
|
||||
fun source(): HttpSource {
|
||||
return if (prefs.delegateSources().get()) {
|
||||
enhancedSource
|
||||
} else {
|
||||
originalSource
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.databinding.DescriptionAdapterEhBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
|
||||
@Composable
|
||||
fun EHentaiDescription(state: MangaScreenState.Success, openMetadataViewer: () -> Unit, search: (String) -> Unit) {
|
||||
@@ -29,7 +29,7 @@ fun EHentaiDescription(state: MangaScreenState.Success, openMetadataViewer: () -
|
||||
val binding = DescriptionAdapterEhBinding.bind(it)
|
||||
|
||||
binding.genre.text =
|
||||
meta.genre?.let { MetadataUtil.getGenreAndColour(context, it) }
|
||||
meta.genre?.let { MetadataUIUtil.getGenreAndColour(context, it) }
|
||||
?.let {
|
||||
binding.genre.setBackgroundColor(it.first)
|
||||
it.second
|
||||
@@ -61,7 +61,7 @@ fun EHentaiDescription(state: MangaScreenState.Success, openMetadataViewer: () -
|
||||
val ratingFloat = meta.averageRating?.toFloat()
|
||||
binding.ratingBar.rating = ratingFloat ?: 0F
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUtil.getRatingString(context, ratingFloat?.times(2))
|
||||
binding.rating.text = (ratingFloat ?: 0F).toString() + " - " + MetadataUIUtil.getRatingString(context, ratingFloat?.times(2))
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.DescriptionAdapter8mBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.EightMusesSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
|
||||
@Composable
|
||||
fun EightMusesDescription(state: MangaScreenState.Success, openMetadataViewer: () -> Unit) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.DescriptionAdapterHbBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.HBrowseSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
|
||||
@Composable
|
||||
fun HBrowseDescription(state: MangaScreenState.Success, openMetadataViewer: () -> Unit) {
|
||||
|
||||
@@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.databinding.DescriptionAdapterHiBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.HitomiSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
@@ -28,7 +28,7 @@ fun HitomiDescription(state: MangaScreenState.Success, openMetadataViewer: () ->
|
||||
if (meta == null || meta !is HitomiSearchMetadata) return@AndroidView
|
||||
val binding = DescriptionAdapterHiBinding.bind(it)
|
||||
|
||||
binding.genre.text = meta.genre?.let { MetadataUtil.getGenreAndColour(context, it) }?.let {
|
||||
binding.genre.text = meta.genre?.let { MetadataUIUtil.getGenreAndColour(context, it) }?.let {
|
||||
binding.genre.setBackgroundColor(it.first)
|
||||
it.second
|
||||
} ?: meta.genre ?: context.getString(R.string.unknown)
|
||||
|
||||
@@ -12,9 +12,9 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.DescriptionAdapterMdBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil.getRatingString
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.getRatingString
|
||||
import kotlin.math.round
|
||||
|
||||
@Composable
|
||||
|
||||
Executable → Regular
+9
-64
@@ -1,72 +1,17 @@
|
||||
package exh.metadata
|
||||
package exh.ui.metadata.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.R
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import exh.util.SourceTagsUtil
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Metadata utils
|
||||
*/
|
||||
object MetadataUtil {
|
||||
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: Long = 1000
|
||||
private const val KIB_FACTOR: Long = 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
|
||||
|
||||
fun parseHumanReadableByteCount(bytes: String): Double? {
|
||||
val ret = bytes.substringBefore(' ').toDouble()
|
||||
return when (bytes.substringAfter(' ')) {
|
||||
"GB" -> ret * GB_FACTOR
|
||||
"GiB" -> ret * GIB_FACTOR
|
||||
"MB" -> ret * MB_FACTOR
|
||||
"MiB" -> ret * MIB_FACTOR
|
||||
"KB" -> ret * KB_FACTOR
|
||||
"KiB" -> ret * KIB_FACTOR
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val ONGOING_SUFFIX = arrayOf(
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}",
|
||||
"<ongoing>",
|
||||
"ongoing",
|
||||
"[incomplete]",
|
||||
"(incomplete)",
|
||||
"{incomplete}",
|
||||
"<incomplete>",
|
||||
"incomplete",
|
||||
"[wip]",
|
||||
"(wip)",
|
||||
"{wip}",
|
||||
"<wip>",
|
||||
"wip",
|
||||
)
|
||||
|
||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
|
||||
|
||||
object MetadataUIUtil {
|
||||
fun getRatingString(context: Context, @FloatRange(from = 0.0, to = 10.0) rating: Float? = null) = when (rating?.roundToInt()) {
|
||||
0 -> R.string.rating0
|
||||
1 -> R.string.rating1
|
||||
@@ -103,12 +48,12 @@ object MetadataUtil {
|
||||
}?.let { (genreColor, stringId) ->
|
||||
genreColor.color to context.getString(stringId)
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView.bindDrawable(context: Context, @DrawableRes drawable: Int) {
|
||||
ContextCompat.getDrawable(context, drawable)?.apply {
|
||||
setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
setBounds(0, 0, 20.dpToPx, 20.dpToPx)
|
||||
setCompoundDrawables(this, null, null, null)
|
||||
fun TextView.bindDrawable(context: Context, @DrawableRes drawable: Int) {
|
||||
ContextCompat.getDrawable(context, drawable)?.apply {
|
||||
setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
setBounds(0, 0, 20.dpToPx, 20.dpToPx)
|
||||
setCompoundDrawables(this, null, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.databinding.DescriptionAdapterNhBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.NHentaiSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
@@ -32,7 +32,7 @@ fun NHentaiDescription(state: MangaScreenState.Success, openMetadataViewer: () -
|
||||
binding.genre.text = meta.tags.filter { it.namespace == NHentaiSearchMetadata.NHENTAI_CATEGORIES_NAMESPACE }.let { tags ->
|
||||
if (tags.isNotEmpty()) tags.joinToString(transform = { it.name }) else null
|
||||
}.let { categoriesString ->
|
||||
categoriesString?.let { MetadataUtil.getGenreAndColour(context, it) }?.let {
|
||||
categoriesString?.let { MetadataUIUtil.getGenreAndColour(context, it) }?.let {
|
||||
binding.genre.setBackgroundColor(it.first)
|
||||
it.second
|
||||
} ?: categoriesString ?: context.getString(R.string.unknown)
|
||||
|
||||
@@ -11,9 +11,8 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.DescriptionAdapterPeBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import exh.metadata.MetadataUtil
|
||||
import exh.metadata.bindDrawable
|
||||
import exh.metadata.metadata.PervEdenSearchMetadata
|
||||
import exh.ui.metadata.adapters.MetadataUIUtil.bindDrawable
|
||||
import java.util.Locale
|
||||
import kotlin.math.round
|
||||
|
||||
@@ -30,7 +29,7 @@ fun PervEdenDescription(state: MangaScreenState.Success, openMetadataViewer: ()
|
||||
if (meta == null || meta !is PervEdenSearchMetadata) return@AndroidView
|
||||
val binding = DescriptionAdapterPeBinding.bind(it)
|
||||
|
||||
binding.genre.text = meta.genre?.let { MetadataUtil.getGenreAndColour(context, it) }?.let {
|
||||
binding.genre.text = meta.genre?.let { MetadataUIUtil.getGenreAndColour(context, it) }?.let {
|
||||
binding.genre.setBackgroundColor(it.first)
|
||||
it.second
|
||||
} ?: meta.genre ?: context.getString(R.string.unknown)
|
||||
@@ -45,7 +44,7 @@ fun PervEdenDescription(state: MangaScreenState.Success, openMetadataViewer: ()
|
||||
|
||||
binding.ratingBar.rating = meta.rating ?: 0F
|
||||
@SuppressLint("SetTextI18n")
|
||||
binding.rating.text = (round((meta.rating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUtil.getRatingString(context, meta.rating?.times(2))
|
||||
binding.rating.text = (round((meta.rating ?: 0F) * 100.0) / 100.0).toString() + " - " + MetadataUIUtil.getRatingString(context, meta.rating?.times(2))
|
||||
|
||||
binding.moreInfo.bindDrawable(context, R.drawable.ic_info_24dp)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user