123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- 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.awaitSuccess
- import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
- 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 okhttp3.Headers
- import okhttp3.OkHttpClient
- import okhttp3.Request
- import okhttp3.Response
- import rx.Observable
- import tachiyomi.core.util.lang.awaitSingle
- 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.
- */
- @Suppress("unused")
- abstract class HttpSource : CatalogueSource {
- /**
- * Network service.
- */
- protected val network: NetworkHelper by injectLazy()
- /**
- * 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 `"${name.lowercase()}/$lang/$versionId"`.
- *
- * The ID is generated by the [generateId] function, which can be reused if needed
- * to generate outdated IDs for cases where the source name or language needs to
- * be changed but migrations can be avoided.
- *
- * Note: the generated ID sets the sign bit to `0`.
- */
- override val id by lazy { generateId(name, lang, versionId) }
- /**
- * Headers used for requests.
- */
- val headers: Headers by lazy { headersBuilder().build() }
- /**
- * Default network client for doing requests.
- */
- open val client: OkHttpClient
- get() = network.client
- /**
- * Generates a unique ID for the source based on the provided [name], [lang] and
- * [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
- * `"${name.lowercase()}/$lang/$versionId"`.
- *
- * Note: the generated ID sets the sign bit to `0`.
- *
- * Can be used to generate outdated IDs, such as when the source name or language
- * needs to be changed but migrations can be avoided.
- *
- * @since extensions-lib 1.5
- * @param name [String] the name of the source
- * @param lang [String] the language of the source
- * @param versionId [Int] the version ID of the source
- * @return a unique ID for the source
- */
- @Suppress("MemberVisibilityCanBePrivate")
- protected fun generateId(name: String, lang: String, versionId: Int): Long {
- val key = "${name.lowercase()}/$lang/$versionId"
- val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
- return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
- }
- /**
- * Headers builder for requests. Implementations can override this method for custom headers.
- */
- protected open fun headersBuilder() = Headers.Builder().apply {
- add("User-Agent", network.defaultUserAgentProvider())
- }
- /**
- * 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.
- */
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
- 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.
- */
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
- 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.
- */
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
- 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.
- */
- protected abstract fun latestUpdatesRequest(page: Int): Request
- /**
- * Parses the response from the site and returns a [MangasPage] object.
- *
- * @param response the response from the site.
- */
- protected abstract fun latestUpdatesParse(response: Response): MangasPage
- /**
- * Get the updated details for a manga.
- * Normally it's not needed to override this method.
- *
- * @param manga the manga to update.
- * @return the updated manga.
- */
- @Suppress("DEPRECATION")
- override suspend fun getMangaDetails(manga: SManga): SManga {
- return fetchMangaDetails(manga).awaitSingle()
- }
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
- 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
- /**
- * Get all the available chapters for a manga.
- * Normally it's not needed to override this method.
- *
- * @param manga the manga to update.
- * @return the chapters for the manga.
- * @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
- */
- @Suppress("DEPRECATION")
- override suspend fun getChapterList(manga: SManga): List<SChapter> {
- if (manga.status == SManga.LICENSED) {
- throw LicensedMangaChaptersException()
- }
- return fetchChapterList(manga).awaitSingle()
- }
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
- 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(LicensedMangaChaptersException())
- }
- }
- /**
- * 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>
- /**
- * Parses the response from the site and returns a SChapter Object.
- *
- * @param response the response from the site.
- */
- protected abstract fun chapterPageParse(response: Response): SChapter
- /**
- * Get the list of pages a chapter has. Pages should be returned
- * in the expected order; the index is ignored.
- *
- * @param chapter the chapter.
- * @return the pages for the chapter.
- */
- @Suppress("DEPRECATION")
- override suspend fun getPageList(chapter: SChapter): List<Page> {
- return fetchPageList(chapter).awaitSingle()
- }
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
- 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.
- *
- * @since extensions-lib 1.5
- * @param page the page whose source image has to be fetched.
- */
- @Suppress("DEPRECATION")
- open suspend fun getImageUrl(page: Page): String {
- return fetchImageUrl(page).awaitSingle()
- }
- @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
- 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 the response of the source image.
- * Typically does not need to be overridden.
- *
- * @since extensions-lib 1.5
- * @param page the page whose source image has to be downloaded.
- */
- open suspend fun getImage(page: Page): Response {
- return client.newCachelessCallWithProgress(imageRequest(page), page)
- .awaitSuccess()
- }
- /**
- * 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
- }
- }
- /**
- * Returns the url of the provided manga
- *
- * @since extensions-lib 1.4
- * @param manga the manga
- * @return url of the manga
- */
- open fun getMangaUrl(manga: SManga): String {
- return mangaDetailsRequest(manga).url.toString()
- }
- /**
- * Returns the url of the provided chapter
- *
- * @since extensions-lib 1.4
- * @param chapter the chapter
- * @return url of the chapter
- */
- open fun getChapterUrl(chapter: SChapter): String {
- return pageListRequest(chapter).url.toString()
- }
- /**
- * 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()
- }
- class LicensedMangaChaptersException : RuntimeException()
|