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:
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
|
||||
interface ConfigurableSource : Source {
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
/**
|
||||
* A source that explicitly doesn't require traffic considerations.
|
||||
*
|
||||
* This typically applies for self-hosted sources.
|
||||
*/
|
||||
interface UnmeteredSource
|
||||
@@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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 <--
|
||||
@@ -0,0 +1,67 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,14 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
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
|
||||
val originalAuthor: String?
|
||||
val originalArtist: String?
|
||||
val originalDescription: String?
|
||||
val originalGenre: String?
|
||||
val originalStatus: Int
|
||||
// SY <--
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
// EXH -->
|
||||
if (other.title.isNotBlank() && originalTitle != other.title) {
|
||||
title = other.originalTitle
|
||||
}
|
||||
// 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 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,39 @@
|
||||
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
|
||||
|
||||
// SY -->
|
||||
override val originalTitle: String
|
||||
get() = title
|
||||
override val originalAuthor: String?
|
||||
get() = author
|
||||
override val originalArtist: String?
|
||||
get() = artist
|
||||
override val originalDescription: String?
|
||||
get() = description
|
||||
override val originalGenre: String?
|
||||
get() = genre
|
||||
override val originalStatus: Int
|
||||
get() = status
|
||||
// SY <--
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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>>
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.app.Application
|
||||
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.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 } todo
|
||||
.maybeInjectEHLogger()
|
||||
.build()
|
||||
|
||||
override val cloudflareClient: OkHttpClient
|
||||
get() = delegate?.networkCloudflareClient ?: network.cloudflareClient
|
||||
.newBuilder()
|
||||
//.injectPatches { id } todo
|
||||
.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<NetworkHelper>().preferences.getBoolean("eh_delegate_sources", true)) { // todo
|
||||
field
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun bindDelegate(delegate: DelegatedHttpSource) {
|
||||
this.delegate = delegate
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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) }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.metadata.metadata.base.FlatMetadata
|
||||
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 {
|
||||
interface GetMangaId {
|
||||
suspend fun awaitId(url: String, sourceId: Long): Long?
|
||||
}
|
||||
interface InsertFlatMetadata {
|
||||
suspend fun await(metadata: RaisedSearchMetadata)
|
||||
}
|
||||
interface GetFlatMetadataById {
|
||||
suspend fun await(id: Long): FlatMetadata?
|
||||
}
|
||||
val getMangaId: GetMangaId 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
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun SManga.id() = getMangaId.awaitId(url, id)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface NamespaceSource : Source
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
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.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface RandomMangaSource : Source {
|
||||
suspend fun fetchRandomMangaUrl(): String
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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())
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package exh.md.utils
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.source.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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package exh.metadata
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.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 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<NetworkHelper>().preferences.getBoolean("use_jp_title", false) } // todo
|
||||
?: 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.copy
|
||||
import exh.md.utils.MangaDexRelation
|
||||
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 { "/manga/$it" }
|
||||
|
||||
val title = title
|
||||
|
||||
val cover = cover
|
||||
|
||||
val author = authors?.joinToString()
|
||||
|
||||
val artist = artists?.joinToString()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.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/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.source.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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.source.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RaisedTag(
|
||||
val namespace: String?,
|
||||
val name: String,
|
||||
val type: Int,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package exh.metadata.metadata.base
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RaisedTitle(
|
||||
val title: String,
|
||||
val type: Int = 0,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,297 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
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.Response
|
||||
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
class EnhancedHttpSource(
|
||||
val originalSource: HttpSource,
|
||||
val enhancedSource: HttpSource,
|
||||
val delegateSources:() -> Boolean
|
||||
) : 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() = 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 (delegateSources()) {
|
||||
enhancedSource
|
||||
} else {
|
||||
originalSource
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user