diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeCatalogueSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeCatalogueSource.kt new file mode 100644 index 00000000..c932f903 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeCatalogueSource.kt @@ -0,0 +1,46 @@ +package eu.kanade.tachiyomi.source + +import eu.kanade.tachiyomi.source.model.AnimesPage +import eu.kanade.tachiyomi.source.model.FilterList +import rx.Observable + +interface AnimeCatalogueSource : AnimeSource { + + /** + * An ISO 639-1 compliant language code (two letters in lower case). + */ + val lang: String + + /** + * Whether the source has support for latest updates. + */ + val supportsLatest: Boolean + + /** + * Returns an observable containing a page with a list of anime. + * + * @param page the page number to retrieve. + */ + fun fetchPopularAnime(page: Int): Observable + + /** + * Returns an observable containing a page with a list of anime. + * + * @param page the page number to retrieve. + * @param query the search query. + * @param filters the list of filters to apply. + */ + fun fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable + + /** + * Returns an observable containing a page with a list of latest anime updates. + * + * @param page the page number to retrieve. + */ + fun fetchLatestUpdates(page: Int): Observable + + /** + * Returns the list of filters for the source. + */ + fun getFilterList(): FilterList +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSource.kt new file mode 100644 index 00000000..e61132c8 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSource.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.source + +import eu.kanade.tachiyomi.source.model.SAnime +import eu.kanade.tachiyomi.source.model.SEpisode +import rx.Observable + +/** + * A basic interface for creating a source. It could be an online source, a local source, etc... + */ +interface AnimeSource { + + /** + * Id for the source. Must be unique. + */ + val id: Long + + /** + * Name of the source. + */ + val name: String + + /** + * Returns an observable with the updated details for a anime. + * + * @param anime the anime to update. + */ + @Deprecated("Use getAnimeDetails instead") + fun fetchAnimeDetails(anime: SAnime): Observable + + /** + * Returns an observable with all the available episodes for an anime. + * + * @param anime the anime to update. + */ + @Deprecated("Use getEpisodeList instead") + fun fetchEpisodeList(anime: SAnime): Observable> + + /** + * Returns an observable with a link for the episode of an anime. + * + * @param episode the episode to get the link for. + */ + @Deprecated("Use getEpisodeList instead") + fun fetchEpisodeLink(episode: SEpisode): Observable + +// /** +// * [1.x API] Get the updated details for a anime. +// */ +// @Suppress("DEPRECATION") +// override suspend fun getAnimeDetails(anime: AnimeInfo): AnimeInfo { +// val sAnime = anime.toSAnime() +// val networkAnime = fetchAnimeDetails(sAnime).awaitSingle() +// sAnime.copyFrom(networkAnime) +// return sAnime.toAnimeInfo() +// } + +// /** +// * [1.x API] Get all the available episodes for a anime. +// */ +// @Suppress("DEPRECATION") +// override suspend fun getEpisodeList(anime: AnimeInfo): List { +// return fetchEpisodeList(anime.toSAnime()).awaitSingle() +// .map { it.toEpisodeInfo() } +// } + +// /** +// * [1.x API] Get a link for the episode of an anime. +// */ +// @Suppress("DEPRECATION") +// override suspend fun getEpisodeLink(episode: EpisodeInfo): String { +// return fetchEpisodeLink(episode.toSEpisode()).awaitSingle() +// } +} + +// fun AnimeSource.icon(): Drawable? = Injekt.get().getAppIconForSource(this) + +// fun AnimeSource.getPreferenceKey(): String = "source_$id" diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceFactory.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceFactory.kt new file mode 100644 index 00000000..72c0c968 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceFactory.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.source + +/** + * A factory for creating sources at runtime. + */ +interface AnimeSourceFactory { + /** + * Create a new copy of the sources + * @return The created sources + */ + fun createSources(): List +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceManager.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceManager.kt new file mode 100644 index 00000000..2a2fbf3d --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/AnimeSourceManager.kt @@ -0,0 +1,76 @@ +package eu.kanade.tachiyomi.source + +import android.content.Context +import eu.kanade.tachiyomi.source.model.SAnime +import eu.kanade.tachiyomi.source.model.SEpisode +import eu.kanade.tachiyomi.source.online.AnimeHttpSource +import rx.Observable + +open class AnimeSourceManager(private val context: Context) { + + private val sourcesMap = mutableMapOf() + + private val stubSourcesMap = mutableMapOf() + + init { + createInternalSources().forEach { registerSource(it) } + } + + open fun get(sourceKey: Long): AnimeSource? { + return sourcesMap[sourceKey] + } + + fun getOrStub(sourceKey: Long): AnimeSource { + return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { + StubSource(sourceKey) + } + } + + fun getOnlineSources() = sourcesMap.values.filterIsInstance() + + fun getCatalogueSources() = sourcesMap.values.filterIsInstance() + + internal fun registerSource(source: AnimeSource) { + if (!sourcesMap.containsKey(source.id)) { + sourcesMap[source.id] = source + } + if (!stubSourcesMap.containsKey(source.id)) { + stubSourcesMap[source.id] = StubSource(source.id) + } + } + + internal fun unregisterSource(source: AnimeSource) { + sourcesMap.remove(source.id) + } + + private fun createInternalSources(): List = listOf( +// LocalAnimeSource(context) + ) + + inner class StubSource(override val id: Long) : AnimeSource { + + override val name: String + get() = id.toString() + + override fun fetchAnimeDetails(anime: SAnime): Observable { + return Observable.error(getSourceNotInstalledException()) + } + + override fun fetchEpisodeList(anime: SAnime): Observable> { + return Observable.error(getSourceNotInstalledException()) + } + + override fun fetchEpisodeLink(episode: SEpisode): Observable { + return Observable.error(getSourceNotInstalledException()) + } + + override fun toString(): String { + return name + } + + private fun getSourceNotInstalledException(): Exception { +// return Exception(context.getString(R.string.source_not_installed, id.toString())) + return Exception("source not found") + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableAnimeSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableAnimeSource.kt new file mode 100644 index 00000000..cdd20408 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/ConfigurableAnimeSource.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.source + +import android.support.v7.preference.PreferenceScreen + +interface ConfigurableAnimeSource : AnimeSource { + + fun setupPreferenceScreen(screen: PreferenceScreen) +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt index 2c016435..4dce2c51 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -1,13 +1,9 @@ package eu.kanade.tachiyomi.source -// import android.graphics.drawable.Drawable -// import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import rx.Observable -// import uy.kohesive.injekt.Injekt -// import uy.kohesive.injekt.api.get /** * A basic interface for creating a source. It could be an online source, a local source, etc... diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/SourceManager.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/SourceManager.kt index 177969fc..d9b6b2fb 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/SourceManager.kt @@ -1,14 +1,13 @@ package eu.kanade.tachiyomi.source -// import android.content.Context -// import eu.kanade.tachiyomi.R +import android.content.Context 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 rx.Observable -open class SourceManager() { +open class SourceManager(private val context: Context) { private val sourcesMap = mutableMapOf() diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/AnimesPage.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/AnimesPage.kt new file mode 100644 index 00000000..f083eec7 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/AnimesPage.kt @@ -0,0 +1,3 @@ +package eu.kanade.tachiyomi.source.model + +data class AnimesPage(val animes: List, val hasNextPage: Boolean) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnime.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnime.kt new file mode 100644 index 00000000..d7107755 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnime.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.source.model + +import java.io.Serializable + +interface SAnime : 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 copyFrom(other: SAnime) { + if (other.title != null) { + title = other.title + } + + if (other.author != null) { + author = other.author + } + + if (other.artist != null) { + artist = other.artist + } + + if (other.description != null) { + description = other.description + } + + if (other.genre != null) { + genre = other.genre + } + + if (other.thumbnail_url != null) { + thumbnail_url = other.thumbnail_url + } + + status = other.status + + if (!initialized) { + initialized = other.initialized + } + } + + companion object { + const val UNKNOWN = 0 + const val ONGOING = 1 + const val COMPLETED = 2 + const val LICENSED = 3 + + fun create(): SAnime { + return SAnimeImpl() + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnimeImpl.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnimeImpl.kt new file mode 100644 index 00000000..2b47339e --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SAnimeImpl.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.source.model + +class SAnimeImpl : SAnime { + + override lateinit var url: String + + override lateinit var title: String + + 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 +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisode.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisode.kt new file mode 100644 index 00000000..9bd4e0c8 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisode.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.source.model + +import java.io.Serializable + +interface SEpisode : Serializable { + + var url: String + + var name: String + + var date_upload: Long + + var episode_number: Float + + var scanlator: String? + + fun copyFrom(other: SEpisode) { + name = other.name + url = other.url + date_upload = other.date_upload + episode_number = other.episode_number + scanlator = other.scanlator + } + + companion object { + fun create(): SEpisode { + return SEpisodeImpl() + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisodeImpl.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisodeImpl.kt new file mode 100644 index 00000000..3e4f53dd --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SEpisodeImpl.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.source.model + +class SEpisodeImpl : SEpisode { + + override lateinit var url: String + + override lateinit var name: String + + override var date_upload: Long = 0 + + override var episode_number: Float = -1f + + override var scanlator: String? = null +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSource.kt new file mode 100644 index 00000000..d4af1269 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSource.kt @@ -0,0 +1,388 @@ +package eu.kanade.tachiyomi.source.online + +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.AnimeCatalogueSource +import eu.kanade.tachiyomi.source.model.AnimesPage +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SAnime +import eu.kanade.tachiyomi.source.model.SEpisode +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.net.URI +import java.net.URISyntaxException +import java.security.MessageDigest + +/** + * A simple implementation for sources from a website. + */ +abstract class AnimeHttpSource : AnimeCatalogueSource { + + /** + * Network service. + */ + protected val network: NetworkHelper by injectLazy() + +// /** +// * Preferences that a source may need. +// */ +// val preferences: SharedPreferences by lazy { +// Injekt.get().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) +// } + + /** + * 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.toLowerCase()}/$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. + */ + val headers: Headers by lazy { headersBuilder().build() } + + /** + * Default network client for doing requests. + */ + open val client: OkHttpClient + get() = network.client + + /** + * Headers builder for requests. Implementations can override this method for custom headers. + */ + protected open fun headersBuilder() = Headers.Builder().apply { + add("User-Agent", DEFAULT_USER_AGENT) + } + + /** + * Visible name of the source. + */ + override fun toString() = "$name (${lang.toUpperCase()})" + + /** + * Returns an observable containing a page with a list of anime. Normally it's not needed to + * override this method. + * + * @param page the page number to retrieve. + */ + override fun fetchPopularAnime(page: Int): Observable { + return client.newCall(popularAnimeRequest(page)) + .asObservableSuccess() + .map { response -> + popularAnimeParse(response) + } + } + + /** + * Returns the request for the popular anime given the page. + * + * @param page the page number to retrieve. + */ + protected abstract fun popularAnimeRequest(page: Int): Request + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + protected abstract fun popularAnimeParse(response: Response): AnimesPage + + /** + * Returns an observable containing a page with a list of anime. 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 fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchAnimeRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchAnimeParse(response) + } + } + + /** + * Returns the request for the search anime 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 searchAnimeRequest(page: Int, query: String, filters: FilterList): Request + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + protected abstract fun searchAnimeParse(response: Response): AnimesPage + + /** + * Returns an observable containing a page with a list of latest anime updates. + * + * @param page the page number to retrieve. + */ + override fun fetchLatestUpdates(page: Int): Observable { + return client.newCall(latestUpdatesRequest(page)) + .asObservableSuccess() + .map { response -> + latestUpdatesParse(response) + } + } + + /** + * Returns the request for latest anime given the page. + * + * @param page the page number to retrieve. + */ + protected abstract fun latestUpdatesRequest(page: Int): Request + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + protected abstract fun latestUpdatesParse(response: Response): AnimesPage + + /** + * Returns an observable with the updated details for a anime. Normally it's not needed to + * override this method. + * + * @param anime the anime to be updated. + */ + override fun fetchAnimeDetails(anime: SAnime): Observable { + return client.newCall(animeDetailsRequest(anime)) + .asObservableSuccess() + .map { response -> + animeDetailsParse(response).apply { initialized = true } + } + } + + /** + * Returns the request for the details of a anime. Override only if it's needed to change the + * url, send different headers or request method like POST. + * + * @param anime the anime to be updated. + */ + open fun animeDetailsRequest(anime: SAnime): Request { + return GET(baseUrl + anime.url, headers) + } + + /** + * Parses the response from the site and returns the details of a anime. + * + * @param response the response from the site. + */ + protected abstract fun animeDetailsParse(response: Response): SAnime + + /** + * Returns an observable with the updated episode list for a anime. Normally it's not needed to + * override this method. If a anime is licensed an empty episode list observable is returned + * + * @param anime the anime to look for episodes. + */ + override fun fetchEpisodeList(anime: SAnime): Observable> { + return if (anime.status != SAnime.LICENSED) { + client.newCall(episodeListRequest(anime)) + .asObservableSuccess() + .map { response -> + episodeListParse(response) + } + } else { + Observable.error(Exception("Licensed - No episodes to show")) + } + } + + override fun fetchEpisodeLink(episode: SEpisode): Observable { + return client.newCall(episodeLinkRequest(episode)) + .asObservableSuccess() + .map { response -> + episodeLinkParse(response) + } + } + + /** + * Returns the request for updating the episode list. Override only if it's needed to override + * the url, send different headers or request method like POST. + * + * @param anime the anime to look for episodes. + */ + protected open fun episodeListRequest(anime: SAnime): Request { + return GET(baseUrl + anime.url, headers) + } + + /** + * Returns the request for getting the episode link. Override only if it's needed to override + * the url, send different headers or request method like POST. + * + * @param episode the episode to look for links. + */ + protected open fun episodeLinkRequest(episode: SEpisode): Request { + return GET(baseUrl + episode.url, headers) + } + + /** + * Parses the response from the site and returns a list of episodes. + * + * @param response the response from the site. + */ + protected abstract fun episodeListParse(response: Response): List + + /** + * Parses the response from the site and returns a list of episodes. + * + * @param response the response from the site. + */ + protected abstract fun episodeLinkParse(response: Response): String + + /** + * 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 episode the episode whose page list has to be fetched. + */ + protected open fun pageListRequest(episode: SEpisode): Request { + return GET(baseUrl + episode.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 + + /** + * 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 { + 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 episode 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. + */ + fun fetchImage(page: Page): Observable { + return client.newCallWithProgress(imageRequest(page), 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 episode whose page list has to be fetched + */ + protected open fun imageRequest(page: Page): Request { + return GET(page.imageUrl!!, headers) + } + + /** + * Assigns the url of the episode 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 episode. + */ + fun SEpisode.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) + } + + /** + * Assigns the url of the anime 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 anime. + */ + fun SAnime.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) + 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 episode into database. Use it if you need to override episode + * fields, like the title or the episode number. Do not change anything to [anime]. + * + * @param episode the episode to be added. + * @param anime the anime of the episode. + */ + open fun prepareNewEpisode(episode: SEpisode, anime: SAnime) { + } + + /** + * Returns the list of filters for the source. + */ + override fun getFilterList() = FilterList() + + companion object { + const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63" + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSourceFetcher.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSourceFetcher.kt new file mode 100644 index 00000000..71ffef79 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/AnimeHttpSourceFetcher.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.model.Page +import rx.Observable + +fun AnimeHttpSource.getImageUrl(page: Page): Observable { + page.status = Page.LOAD_PAGE + return fetchImageUrl(page) + .doOnError { page.status = Page.ERROR } + .onErrorReturn { null } + .doOnNext { page.imageUrl = it } + .map { page } +} + +fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { !it.imageUrl.isNullOrEmpty() } + .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) +} + +fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { it.imageUrl.isNullOrEmpty() } + .concatMap { getImageUrl(it) } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ParsedAnimeHttpSource.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ParsedAnimeHttpSource.kt new file mode 100644 index 00000000..e773124d --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/online/ParsedAnimeHttpSource.kt @@ -0,0 +1,222 @@ +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.model.AnimesPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SAnime +import eu.kanade.tachiyomi.source.model.SEpisode +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * A simple implementation for sources from a website using Jsoup, an HTML parser. + */ +abstract class ParsedAnimeHttpSource : AnimeHttpSource() { + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + override fun popularAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = document.select(popularAnimeSelector()).map { element -> + popularAnimeFromElement(element) + } + + val hasNextPage = popularAnimeNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return AnimesPage(animes, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each anime. + */ + protected abstract fun popularAnimeSelector(): String + + /** + * Returns a anime 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 [popularAnimeSelector]. + */ + protected abstract fun popularAnimeFromElement(element: Element): SAnime + + /** + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. + */ + protected abstract fun popularAnimeNextPageSelector(): String? + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + override fun searchAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = document.select(searchAnimeSelector()).map { element -> + searchAnimeFromElement(element) + } + + val hasNextPage = searchAnimeNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return AnimesPage(animes, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each anime. + */ + protected abstract fun searchAnimeSelector(): String + + /** + * Returns a anime 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 [searchAnimeSelector]. + */ + protected abstract fun searchAnimeFromElement(element: Element): SAnime + + /** + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. + */ + protected abstract fun searchAnimeNextPageSelector(): String? + + /** + * Parses the response from the site and returns a [AnimesPage] object. + * + * @param response the response from the site. + */ + override fun latestUpdatesParse(response: Response): AnimesPage { + val document = response.asJsoup() + + val animes = document.select(latestUpdatesSelector()).map { element -> + latestUpdatesFromElement(element) + } + + val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return AnimesPage(animes, hasNextPage) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each anime. + */ + protected abstract fun latestUpdatesSelector(): String + + /** + * Returns a anime 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): SAnime + + /** + * Returns the Jsoup selector that returns the 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 anime. + * + * @param response the response from the site. + */ + override fun animeDetailsParse(response: Response): SAnime { + return animeDetailsParse(response.asJsoup()) + } + + /** + * Returns the details of the anime from the given [document]. + * + * @param document the parsed document. + */ + protected abstract fun animeDetailsParse(document: Document): SAnime + + /** + * Parses the response from the site and returns a list of episodes. + * + * @param response the response from the site. + */ + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + return document.select(episodeListSelector()).map { episodeFromElement(it) } + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each episode. + */ + protected abstract fun episodeListSelector(): String + + /** + * Parses the response from the site and returns a list of episodes. + * + * @param response the response from the site. + */ + override fun episodeLinkParse(response: Response): String { + val document = response.asJsoup() + return linkFromElement(document.select(episodeLinkSelector()).first()) + } + + /** + * Returns the Jsoup selector that returns a list of [Element] corresponding to each episode. + */ + protected abstract fun episodeLinkSelector(): String + + /** + * Returns a episode from the given element. + * + * @param element an element obtained from [episodeListSelector]. + */ + protected abstract fun episodeFromElement(element: Element): SEpisode + + /** + * Returns a episode from the given element. + * + * @param element an element obtained from [episodeListSelector]. + */ + protected abstract fun linkFromElement(element: Element): String + + /** + * Parses the response from the site and returns the page list. + * + * @param response the response from the site. + */ + override fun pageListParse(response: Response): List { + return pageListParse(response.asJsoup()) + } + + /** + * Returns a page list from the given document. + * + * @param document the parsed document. + */ + protected abstract fun pageListParse(document: Document): List + + /** + * 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 +}