HttpSource.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. package eu.kanade.tachiyomi.source.online
  2. import eu.kanade.tachiyomi.network.GET
  3. import eu.kanade.tachiyomi.network.NetworkHelper
  4. import eu.kanade.tachiyomi.network.asObservableSuccess
  5. import eu.kanade.tachiyomi.network.awaitSuccess
  6. import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
  7. import eu.kanade.tachiyomi.source.CatalogueSource
  8. import eu.kanade.tachiyomi.source.model.FilterList
  9. import eu.kanade.tachiyomi.source.model.MangasPage
  10. import eu.kanade.tachiyomi.source.model.Page
  11. import eu.kanade.tachiyomi.source.model.SChapter
  12. import eu.kanade.tachiyomi.source.model.SManga
  13. import okhttp3.Headers
  14. import okhttp3.OkHttpClient
  15. import okhttp3.Request
  16. import okhttp3.Response
  17. import rx.Observable
  18. import tachiyomi.core.util.lang.awaitSingle
  19. import uy.kohesive.injekt.injectLazy
  20. import java.net.URI
  21. import java.net.URISyntaxException
  22. import java.security.MessageDigest
  23. /**
  24. * A simple implementation for sources from a website.
  25. */
  26. @Suppress("unused")
  27. abstract class HttpSource : CatalogueSource {
  28. /**
  29. * Network service.
  30. */
  31. protected val network: NetworkHelper by injectLazy()
  32. /**
  33. * Base url of the website without the trailing slash, like: http://mysite.com
  34. */
  35. abstract val baseUrl: String
  36. /**
  37. * Version id used to generate the source id. If the site completely changes and urls are
  38. * incompatible, you may increase this value and it'll be considered as a new source.
  39. */
  40. open val versionId = 1
  41. /**
  42. * ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
  43. * of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
  44. *
  45. * The ID is generated by the [generateId] function, which can be reused if needed
  46. * to generate outdated IDs for cases where the source name or language needs to
  47. * be changed but migrations can be avoided.
  48. *
  49. * Note: the generated ID sets the sign bit to `0`.
  50. */
  51. override val id by lazy { generateId(name, lang, versionId) }
  52. /**
  53. * Headers used for requests.
  54. */
  55. val headers: Headers by lazy { headersBuilder().build() }
  56. /**
  57. * Default network client for doing requests.
  58. */
  59. open val client: OkHttpClient
  60. get() = network.client
  61. /**
  62. * Generates a unique ID for the source based on the provided [name], [lang] and
  63. * [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
  64. * `"${name.lowercase()}/$lang/$versionId"`.
  65. *
  66. * Note: the generated ID sets the sign bit to `0`.
  67. *
  68. * Can be used to generate outdated IDs, such as when the source name or language
  69. * needs to be changed but migrations can be avoided.
  70. *
  71. * @since extensions-lib 1.5
  72. * @param name [String] the name of the source
  73. * @param lang [String] the language of the source
  74. * @param versionId [Int] the version ID of the source
  75. * @return a unique ID for the source
  76. */
  77. @Suppress("MemberVisibilityCanBePrivate")
  78. protected fun generateId(name: String, lang: String, versionId: Int): Long {
  79. val key = "${name.lowercase()}/$lang/$versionId"
  80. val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
  81. return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
  82. }
  83. /**
  84. * Headers builder for requests. Implementations can override this method for custom headers.
  85. */
  86. protected open fun headersBuilder() = Headers.Builder().apply {
  87. add("User-Agent", network.defaultUserAgentProvider())
  88. }
  89. /**
  90. * Visible name of the source.
  91. */
  92. override fun toString() = "$name (${lang.uppercase()})"
  93. /**
  94. * Returns an observable containing a page with a list of manga. Normally it's not needed to
  95. * override this method.
  96. *
  97. * @param page the page number to retrieve.
  98. */
  99. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
  100. override fun fetchPopularManga(page: Int): Observable<MangasPage> {
  101. return client.newCall(popularMangaRequest(page))
  102. .asObservableSuccess()
  103. .map { response ->
  104. popularMangaParse(response)
  105. }
  106. }
  107. /**
  108. * Returns the request for the popular manga given the page.
  109. *
  110. * @param page the page number to retrieve.
  111. */
  112. protected abstract fun popularMangaRequest(page: Int): Request
  113. /**
  114. * Parses the response from the site and returns a [MangasPage] object.
  115. *
  116. * @param response the response from the site.
  117. */
  118. protected abstract fun popularMangaParse(response: Response): MangasPage
  119. /**
  120. * Returns an observable containing a page with a list of manga. Normally it's not needed to
  121. * override this method.
  122. *
  123. * @param page the page number to retrieve.
  124. * @param query the search query.
  125. * @param filters the list of filters to apply.
  126. */
  127. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
  128. override fun fetchSearchManga(
  129. page: Int,
  130. query: String,
  131. filters: FilterList,
  132. ): Observable<MangasPage> {
  133. return Observable.defer {
  134. try {
  135. client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
  136. } catch (e: NoClassDefFoundError) {
  137. // RxJava doesn't handle Errors, which tends to happen during global searches
  138. // if an old extension using non-existent classes is still around
  139. throw RuntimeException(e)
  140. }
  141. }
  142. .map { response ->
  143. searchMangaParse(response)
  144. }
  145. }
  146. /**
  147. * Returns the request for the search manga given the page.
  148. *
  149. * @param page the page number to retrieve.
  150. * @param query the search query.
  151. * @param filters the list of filters to apply.
  152. */
  153. protected abstract fun searchMangaRequest(
  154. page: Int,
  155. query: String,
  156. filters: FilterList,
  157. ): Request
  158. /**
  159. * Parses the response from the site and returns a [MangasPage] object.
  160. *
  161. * @param response the response from the site.
  162. */
  163. protected abstract fun searchMangaParse(response: Response): MangasPage
  164. /**
  165. * Returns an observable containing a page with a list of latest manga updates.
  166. *
  167. * @param page the page number to retrieve.
  168. */
  169. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
  170. override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
  171. return client.newCall(latestUpdatesRequest(page))
  172. .asObservableSuccess()
  173. .map { response ->
  174. latestUpdatesParse(response)
  175. }
  176. }
  177. /**
  178. * Returns the request for latest manga given the page.
  179. *
  180. * @param page the page number to retrieve.
  181. */
  182. protected abstract fun latestUpdatesRequest(page: Int): Request
  183. /**
  184. * Parses the response from the site and returns a [MangasPage] object.
  185. *
  186. * @param response the response from the site.
  187. */
  188. protected abstract fun latestUpdatesParse(response: Response): MangasPage
  189. /**
  190. * Get the updated details for a manga.
  191. * Normally it's not needed to override this method.
  192. *
  193. * @param manga the manga to update.
  194. * @return the updated manga.
  195. */
  196. @Suppress("DEPRECATION")
  197. override suspend fun getMangaDetails(manga: SManga): SManga {
  198. return fetchMangaDetails(manga).awaitSingle()
  199. }
  200. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
  201. override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
  202. return client.newCall(mangaDetailsRequest(manga))
  203. .asObservableSuccess()
  204. .map { response ->
  205. mangaDetailsParse(response).apply { initialized = true }
  206. }
  207. }
  208. /**
  209. * Returns the request for the details of a manga. Override only if it's needed to change the
  210. * url, send different headers or request method like POST.
  211. *
  212. * @param manga the manga to be updated.
  213. */
  214. open fun mangaDetailsRequest(manga: SManga): Request {
  215. return GET(baseUrl + manga.url, headers)
  216. }
  217. /**
  218. * Parses the response from the site and returns the details of a manga.
  219. *
  220. * @param response the response from the site.
  221. */
  222. protected abstract fun mangaDetailsParse(response: Response): SManga
  223. /**
  224. * Get all the available chapters for a manga.
  225. * Normally it's not needed to override this method.
  226. *
  227. * @param manga the manga to update.
  228. * @return the chapters for the manga.
  229. * @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
  230. */
  231. @Suppress("DEPRECATION")
  232. override suspend fun getChapterList(manga: SManga): List<SChapter> {
  233. if (manga.status == SManga.LICENSED) {
  234. throw LicensedMangaChaptersException()
  235. }
  236. return fetchChapterList(manga).awaitSingle()
  237. }
  238. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
  239. override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
  240. return if (manga.status != SManga.LICENSED) {
  241. client.newCall(chapterListRequest(manga))
  242. .asObservableSuccess()
  243. .map { response ->
  244. chapterListParse(response)
  245. }
  246. } else {
  247. Observable.error(LicensedMangaChaptersException())
  248. }
  249. }
  250. /**
  251. * Returns the request for updating the chapter list. Override only if it's needed to override
  252. * the url, send different headers or request method like POST.
  253. *
  254. * @param manga the manga to look for chapters.
  255. */
  256. protected open fun chapterListRequest(manga: SManga): Request {
  257. return GET(baseUrl + manga.url, headers)
  258. }
  259. /**
  260. * Parses the response from the site and returns a list of chapters.
  261. *
  262. * @param response the response from the site.
  263. */
  264. protected abstract fun chapterListParse(response: Response): List<SChapter>
  265. /**
  266. * Parses the response from the site and returns a SChapter Object.
  267. *
  268. * @param response the response from the site.
  269. */
  270. protected abstract fun chapterPageParse(response: Response): SChapter
  271. /**
  272. * Get the list of pages a chapter has. Pages should be returned
  273. * in the expected order; the index is ignored.
  274. *
  275. * @param chapter the chapter.
  276. * @return the pages for the chapter.
  277. */
  278. @Suppress("DEPRECATION")
  279. override suspend fun getPageList(chapter: SChapter): List<Page> {
  280. return fetchPageList(chapter).awaitSingle()
  281. }
  282. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
  283. override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
  284. return client.newCall(pageListRequest(chapter))
  285. .asObservableSuccess()
  286. .map { response ->
  287. pageListParse(response)
  288. }
  289. }
  290. /**
  291. * Returns the request for getting the page list. Override only if it's needed to override the
  292. * url, send different headers or request method like POST.
  293. *
  294. * @param chapter the chapter whose page list has to be fetched.
  295. */
  296. protected open fun pageListRequest(chapter: SChapter): Request {
  297. return GET(baseUrl + chapter.url, headers)
  298. }
  299. /**
  300. * Parses the response from the site and returns a list of pages.
  301. *
  302. * @param response the response from the site.
  303. */
  304. protected abstract fun pageListParse(response: Response): List<Page>
  305. /**
  306. * Returns an observable with the page containing the source url of the image. If there's any
  307. * error, it will return null instead of throwing an exception.
  308. *
  309. * @since extensions-lib 1.5
  310. * @param page the page whose source image has to be fetched.
  311. */
  312. @Suppress("DEPRECATION")
  313. open suspend fun getImageUrl(page: Page): String {
  314. return fetchImageUrl(page).awaitSingle()
  315. }
  316. @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
  317. open fun fetchImageUrl(page: Page): Observable<String> {
  318. return client.newCall(imageUrlRequest(page))
  319. .asObservableSuccess()
  320. .map { imageUrlParse(it) }
  321. }
  322. /**
  323. * Returns the request for getting the url to the source image. Override only if it's needed to
  324. * override the url, send different headers or request method like POST.
  325. *
  326. * @param page the chapter whose page list has to be fetched
  327. */
  328. protected open fun imageUrlRequest(page: Page): Request {
  329. return GET(page.url, headers)
  330. }
  331. /**
  332. * Parses the response from the site and returns the absolute url to the source image.
  333. *
  334. * @param response the response from the site.
  335. */
  336. protected abstract fun imageUrlParse(response: Response): String
  337. /**
  338. * Returns the response of the source image.
  339. * Typically does not need to be overridden.
  340. *
  341. * @since extensions-lib 1.5
  342. * @param page the page whose source image has to be downloaded.
  343. */
  344. open suspend fun getImage(page: Page): Response {
  345. return client.newCachelessCallWithProgress(imageRequest(page), page)
  346. .awaitSuccess()
  347. }
  348. /**
  349. * Returns the request for getting the source image. Override only if it's needed to override
  350. * the url, send different headers or request method like POST.
  351. *
  352. * @param page the chapter whose page list has to be fetched
  353. */
  354. protected open fun imageRequest(page: Page): Request {
  355. return GET(page.imageUrl!!, headers)
  356. }
  357. /**
  358. * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
  359. * database and the urls could still work after a domain change.
  360. *
  361. * @param url the full url to the chapter.
  362. */
  363. fun SChapter.setUrlWithoutDomain(url: String) {
  364. this.url = getUrlWithoutDomain(url)
  365. }
  366. /**
  367. * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
  368. * database and the urls could still work after a domain change.
  369. *
  370. * @param url the full url to the manga.
  371. */
  372. fun SManga.setUrlWithoutDomain(url: String) {
  373. this.url = getUrlWithoutDomain(url)
  374. }
  375. /**
  376. * Returns the url of the given string without the scheme and domain.
  377. *
  378. * @param orig the full url.
  379. */
  380. private fun getUrlWithoutDomain(orig: String): String {
  381. return try {
  382. val uri = URI(orig.replace(" ", "%20"))
  383. var out = uri.path
  384. if (uri.query != null) {
  385. out += "?" + uri.query
  386. }
  387. if (uri.fragment != null) {
  388. out += "#" + uri.fragment
  389. }
  390. out
  391. } catch (e: URISyntaxException) {
  392. orig
  393. }
  394. }
  395. /**
  396. * Returns the url of the provided manga
  397. *
  398. * @since extensions-lib 1.4
  399. * @param manga the manga
  400. * @return url of the manga
  401. */
  402. open fun getMangaUrl(manga: SManga): String {
  403. return mangaDetailsRequest(manga).url.toString()
  404. }
  405. /**
  406. * Returns the url of the provided chapter
  407. *
  408. * @since extensions-lib 1.4
  409. * @param chapter the chapter
  410. * @return url of the chapter
  411. */
  412. open fun getChapterUrl(chapter: SChapter): String {
  413. return pageListRequest(chapter).url.toString()
  414. }
  415. /**
  416. * Called before inserting a new chapter into database. Use it if you need to override chapter
  417. * fields, like the title or the chapter number. Do not change anything to [manga].
  418. *
  419. * @param chapter the chapter to be added.
  420. * @param manga the manga of the chapter.
  421. */
  422. open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
  423. /**
  424. * Returns the list of filters for the source.
  425. */
  426. override fun getFilterList() = FilterList()
  427. }
  428. class LicensedMangaChaptersException : RuntimeException()