From dd56d7c0bb6d415f16e9dafa4e3558767d420ad3 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sun, 8 Jan 2017 18:12:19 +0100 Subject: [PATCH] Initial support for external sources --- .../tachiyomi/data/cache/ChapterCache.kt | 18 +- .../data/database/mappers/MangaTypeMapping.kt | 2 +- .../tachiyomi/data/database/models/Chapter.kt | 11 +- .../tachiyomi/data/database/models/Manga.kt | 55 +-- .../data/database/models/MangaImpl.kt | 2 +- .../data/database/queries/MangaQueries.kt | 2 +- .../tachiyomi/data/download/Downloader.kt | 8 +- .../tachiyomi/data/glide/MangaModelLoader.kt | 2 +- .../data/library/LibraryUpdateService.kt | 6 +- .../data/preference/PreferenceKeys.kt | 4 +- .../data/preference/PreferencesHelper.kt | 2 +- .../tachiyomi/data/source/CatalogueSource.kt | 46 +++ .../eu/kanade/tachiyomi/data/source/Source.kt | 18 +- .../tachiyomi/data/source/SourceManager.kt | 132 +++++- .../tachiyomi/data/source/model/Filter.kt | 18 + .../tachiyomi/data/source/model/FilterList.kt | 14 + .../tachiyomi/data/source/model/MangasPage.kt | 12 +- .../tachiyomi/data/source/model/SChapter.kt | 28 ++ .../data/source/model/SChapterImpl.kt | 13 + .../tachiyomi/data/source/model/SManga.kt | 58 +++ .../tachiyomi/data/source/model/SMangaImpl.kt | 23 ++ .../data/source/online/OnlineSource.kt | 381 ++++++------------ .../data/source/online/OnlineSourceFetcher.kt | 98 +++++ .../data/source/online/ParsedOnlineSource.kt | 139 +++---- .../data/source/online/YamlOnlineSource.kt | 118 +++--- .../source/online/YamlOnlineSourceMappings.kt | 10 +- .../data/source/online/english/Batoto.kt | 129 ++---- .../data/source/online/english/Kissmanga.kt | 162 ++++---- .../data/source/online/english/Mangafox.kt | 72 ++-- .../data/source/online/english/Mangahere.kt | 66 +-- .../data/source/online/english/Mangasee.kt | 137 ++----- .../source/online/english/Readmangatoday.kt | 71 ++-- .../data/source/online/german/WieManga.kt | 57 ++- .../data/source/online/russian/Mangachan.kt | 112 ++--- .../data/source/online/russian/Mintmanga.kt | 66 +-- .../data/source/online/russian/Readmanga.kt | 70 ++-- .../ui/catalogue/CatalogueFragment.kt | 35 +- .../ui/catalogue/CatalogueNavigationView.kt | 7 +- .../tachiyomi/ui/catalogue/CataloguePager.kt | 30 +- .../ui/catalogue/CataloguePresenter.kt | 87 ++-- .../ui/catalogue/NoResultsException.kt | 3 + .../eu/kanade/tachiyomi/ui/catalogue/Pager.kt | 22 +- .../ui/latest_updates/LatestUpdatesPager.kt | 24 +- .../latest_updates/LatestUpdatesPresenter.kt | 10 +- .../tachiyomi/ui/library/LibraryPresenter.kt | 2 +- .../ui/manga/chapter/ChaptersPresenter.kt | 2 +- .../ui/manga/info/MangaInfoFragment.kt | 7 +- .../ui/manga/info/MangaInfoPresenter.kt | 3 +- .../tachiyomi/ui/reader/ChapterLoader.kt | 17 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 4 +- .../recent_updates/RecentChaptersPresenter.kt | 2 +- .../ui/setting/SettingsSourcesFragment.kt | 7 +- .../ui/setting/SettingsTrackingFragment.kt | 5 +- .../tachiyomi/util/ChapterSourceSync.kt | 19 +- .../eu/kanade/tachiyomi/util/UrlUtil.java | 26 -- .../kanade/tachiyomi/util/ViewExtensions.kt | 3 +- .../preference/LoginDialogPreference.kt | 4 +- .../widget/preference/SourceLoginDialog.kt | 4 +- app/src/main/res/values/keys.xml | 2 +- .../data/library/LibraryUpdateServiceTest.kt | 10 +- 60 files changed, 1371 insertions(+), 1126 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index 7314ed10c..630a87f9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -5,6 +5,7 @@ import android.text.format.Formatter import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson import com.jakewharton.disklrucache.DiskLruCache +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.saveTo @@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) { /** * Get page list from cache. * - * @param chapterUrl the url of the chapter. + * @param chapter the chapter. * @return an observable of the list of pages. */ - fun getPageListFromCache(chapterUrl: String): Observable> { - return Observable.fromCallable> { + fun getPageListFromCache(chapter: Chapter): Observable> { + return Observable.fromCallable { // Get the key for the chapter. - val key = DiskUtil.hashKeyForDisk(chapterUrl) + val key = DiskUtil.hashKeyForDisk(getKey(chapter)) // Convert JSON string to list of objects. Throws an exception if snapshot is null diskCache.get(key).use { @@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) { /** * Add page list to disk cache. * - * @param chapterUrl the url of the chapter. + * @param chapter the chapter. * @param pages list of pages. */ - fun putPageListToCache(chapterUrl: String, pages: List) { + fun putPageListToCache(chapter: Chapter, pages: List) { // Convert list of pages to json string. val cachedValue = gson.toJson(pages) @@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) { try { // Get editor from md5 key. - val key = DiskUtil.hashKeyForDisk(chapterUrl) + val key = DiskUtil.hashKeyForDisk(getKey(chapter)) editor = diskCache.edit(key) ?: return // Write chapter urls to cache. @@ -196,5 +197,8 @@ class ChapterCache(private val context: Context) { } } + private fun getKey(chapter: Chapter): String { + return "${chapter.manga_id}${chapter.url}" + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index 0d410a2b1..71ca01420 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver() { override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply { id = cursor.getLong(cursor.getColumnIndex(COL_ID)) - source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE)) + source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)) url = cursor.getString(cursor.getColumnIndex(COL_URL)) artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST)) author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index af2346fc8..777f8f221 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -1,17 +1,14 @@ package eu.kanade.tachiyomi.data.database.models +import eu.kanade.tachiyomi.data.source.model.SChapter import java.io.Serializable -interface Chapter : Serializable { +interface Chapter : SChapter, Serializable { var id: Long? var manga_id: Long? - var url: String - - var name: String - var read: Boolean var bookmark: Boolean @@ -20,10 +17,6 @@ interface Chapter : Serializable { var date_fetch: Long - var date_upload: Long - - var chapter_number: Float - var source_order: Int val isRecognizedNumber: Boolean diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 320560aa9..afb026491 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -1,35 +1,17 @@ package eu.kanade.tachiyomi.data.database.models -import java.io.Serializable +import eu.kanade.tachiyomi.data.source.model.SManga -interface Manga : Serializable { +interface Manga : SManga { var id: Long? - var source: Int - - 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 source: Long var favorite: Boolean var last_update: Long - var initialized: Boolean - var viewer: Int var chapter_flags: Int @@ -38,27 +20,6 @@ interface Manga : Serializable { var category: Int - fun copyFrom(other: Manga) { - 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 - - initialized = true - } - fun setChapterOrder(order: Int) { setFlags(order, SORT_MASK) } @@ -94,11 +55,6 @@ interface Manga : Serializable { companion object { - const val UNKNOWN = 0 - const val ONGOING = 1 - const val COMPLETED = 2 - const val LICENSED = 3 - const val SORT_DESC = 0x00000000 const val SORT_ASC = 0x00000001 const val SORT_MASK = 0x00000001 @@ -126,12 +82,13 @@ interface Manga : Serializable { const val DISPLAY_NUMBER = 0x00100000 const val DISPLAY_MASK = 0x00100000 - fun create(source: Int): Manga = MangaImpl().apply { + fun create(source: Long): Manga = MangaImpl().apply { this.source = source } - fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply { + fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply { url = pathUrl + this.title = title this.source = source } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index 000618a3a..0b3121da3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -4,7 +4,7 @@ class MangaImpl : Manga { override var id: Long? = null - override var source: Int = 0 + override var source: Long = 0 override lateinit var url: String diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index e6206835b..b22a2fa33 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -40,7 +40,7 @@ interface MangaQueries : DbProvider { .build()) .prepare() - fun getManga(url: String, sourceId: Int) = db.get() + fun getManga(url: String, sourceId: Long) = db.get() .`object`(Manga::class.java) .withQuery(Query.builder() .table(MangaTable.TABLE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 5c28ec345..b9f8b49d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.plusAssign @@ -251,8 +252,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro val pageListObservable = if (download.pages == null) { // Pull page list from network and add them to download object - download.source.fetchPageListFromNetwork(download.chapter) + download.source.fetchPageList(download.chapter) .doOnNext { pages -> + if (pages.isEmpty()) { + throw Exception("Page list is empty") + } download.pages = pages } } else { @@ -345,7 +349,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable { page.status = Page.DOWNLOAD_IMAGE page.progress = 0 - return source.imageResponse(page) + return source.fetchImage(page) .map { response -> val file = tmpDir.createFile("$filename.tmp") try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt index 429086ff0..0787d427c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt @@ -52,7 +52,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader { /** * Map where request headers are stored for a source. */ - private val cachedHeaders = hashMapOf() + private val cachedHeaders = hashMapOf() /** * Factory class for creating [MangaModelLoader] instances. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 90a51f594..1fc874746 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.source.SourceManager +import eu.kanade.tachiyomi.data.source.model.SManga import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.* @@ -214,7 +215,7 @@ class LibraryUpdateService : Service() { } if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) { - listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED } + listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } } return listToUpdate @@ -328,9 +329,10 @@ class LibraryUpdateService : Service() { ?: return@concatMap Observable.empty() source.fetchMangaDetails(manga) - .doOnNext { networkManga -> + .map { networkManga -> manga.copyFrom(networkManga) db.insertManga(manga).executeAsBlocking() + manga } .onErrorReturn { manga } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 9f0681371..4812b0c72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -91,9 +91,9 @@ class PreferenceKeys(context: Context) { val downloadNew = context.getString(R.string.pref_download_new_key) - fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId" + fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" - fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId" + fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 613fa0504..e1f316262 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -74,7 +74,7 @@ class PreferencesHelper(val context: Context) { fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false) - fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1) + fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1) fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt new file mode 100644 index 000000000..f5b0dbf27 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt @@ -0,0 +1,46 @@ +package eu.kanade.tachiyomi.data.source + +import eu.kanade.tachiyomi.data.source.model.FilterList +import eu.kanade.tachiyomi.data.source.model.MangasPage +import rx.Observable + +interface CatalogueSource : Source { + + /** + * 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 manga. + * + * @param page the page number to retrieve. + */ + fun fetchPopularManga(page: Int): Observable + + /** + * 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 + + /** + * 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 + + /** + * Returns the list of filters for the source. + */ + fun getFilterList(): FilterList +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt index ba196a51f..f1ec5ccb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt @@ -1,8 +1,8 @@ package eu.kanade.tachiyomi.data.source -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.SChapter +import eu.kanade.tachiyomi.data.source.model.SManga import rx.Observable /** @@ -13,7 +13,7 @@ interface Source { /** * Id for the source. Must be unique. */ - val id: Int + val id: Long /** * Name of the source. @@ -25,26 +25,20 @@ interface Source { * * @param manga the manga to update. */ - fun fetchMangaDetails(manga: Manga): Observable + fun fetchMangaDetails(manga: SManga): Observable /** * Returns an observable with all the available chapters for a manga. * * @param manga the manga to update. */ - fun fetchChapterList(manga: Manga): Observable> + fun fetchChapterList(manga: SManga): Observable> /** * Returns an observable with the list of pages a chapter has. * * @param chapter the chapter. */ - fun fetchPageList(chapter: Chapter): Observable> + fun fetchPageList(chapter: SChapter): Observable> - /** - * Returns an observable with the path of the image. - * - * @param page the page. - */ - fun fetchImage(page: Page): Observable } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt index 88751ac2a..84ebd02a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt @@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.data.source import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.os.Environment +import dalvik.system.PathClassLoader import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource @@ -18,29 +21,47 @@ import java.io.File open class SourceManager(private val context: Context) { - private val sourcesMap = createSources() + private val sourcesMap = mutableMapOf() - open fun get(sourceKey: Int): Source? { + init { + createSources() + } + + open fun get(sourceKey: Long): Source? { return sourcesMap[sourceKey] } - fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java) + fun getOnlineSources() = sourcesMap.values.filterIsInstance() - private fun createOnlineSourceList(): List = listOf( - Batoto(1), - Mangahere(2), - Mangafox(3), - Kissmanga(4), - Readmanga(5), - Mintmanga(6), - Mangachan(7), - Readmangatoday(8), - Mangasee(9), - WieManga(10) + fun getCatalogueSources() = sourcesMap.values.filterIsInstance() + + private fun createSources() { + createExtensionSources().forEach { registerSource(it) } + createYamlSources().forEach { registerSource(it) } + createInternalSources().forEach { registerSource(it) } + } + + private fun registerSource(source: Source, overwrite: Boolean = false) { + if (overwrite || !sourcesMap.containsKey(source.id)) { + sourcesMap.put(source.id, source) + } + } + + private fun createInternalSources(): List = listOf( + Batoto(), + Mangahere(), + Mangafox(), + Kissmanga(), + Readmanga(), + Mintmanga(), + Mangachan(), + Readmangatoday(), + Mangasee(), + WieManga() ) - private fun createSources(): Map = hashMapOf().apply { - createOnlineSourceList().forEach { put(it.id, it) } + private fun createYamlSources(): List { + val sources = mutableListOf() val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name), "parsers") @@ -50,12 +71,89 @@ open class SourceManager(private val context: Context) { for (file in parsersDir.listFiles().filter { it.extension == "yml" }) { try { val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) } - YamlOnlineSource(map).let { put(it.id, it) } + sources.add(YamlOnlineSource(map)) } catch (e: Exception) { Timber.e("Error loading source from file. Bad format?") } } } + return sources + } + + private fun createExtensionSources(): List { + val pkgManager = context.packageManager + val flags = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + val installedPkgs = pkgManager.getInstalledPackages(flags) + val extPkgs = installedPkgs.filter { it.reqFeatures.orEmpty().any { it.name == FEATURE } } + + val sources = mutableListOf() + for (pkgInfo in extPkgs) { + val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName, + PackageManager.GET_META_DATA) ?: continue + + + val data = appInfo.metaData + val extName = data.getString(NAME) + val version = data.getInt(VERSION) + val sourceClass = extendClassName(data.getString(SOURCE), pkgInfo.packageName) + + val ext = Extension(extName, appInfo, version, sourceClass) + if (!validateExtension(ext)) { + continue + } + + val instance = loadExtension(ext, pkgManager) + if (instance == null) { + Timber.e("Extension error: failed to instance $extName") + continue + } + sources.add(instance) + } + return sources + } + + private fun validateExtension(ext: Extension): Boolean { + if (ext.version < LIB_VERSION_MIN || ext.version > LIB_VERSION_MAX) { + Timber.e("Extension error: ${ext.name} has version ${ext.version}, while only versions " + + "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed") + return false + } + return true + } + + private fun loadExtension(ext: Extension, pkgManager: PackageManager): OnlineSource? { + return try { + val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader) + val resources = pkgManager.getResourcesForApplication(ext.appInfo) + + Class.forName(ext.sourceClass, false, classLoader).newInstance() as? OnlineSource + } catch (e: Exception) { + null + } catch (e: LinkageError) { + null + } + } + + private fun extendClassName(className: String, packageName: String): String { + return if (className.startsWith(".")) { + packageName + className + } else { + className + } + } + + class Extension(val name: String, + val appInfo: ApplicationInfo, + val version: Int, + val sourceClass: String) + + private companion object { + const val FEATURE = "tachiyomi.extension" + const val NAME = "tachiyomi.extension.name" + const val VERSION = "tachiyomi.extension.version" + const val SOURCE = "tachiyomi.extension.source" + const val LIB_VERSION_MIN = 1 + const val LIB_VERSION_MAX = 1 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt new file mode 100644 index 000000000..b1b9c6d67 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.data.source.model + +sealed class Filter(val name: String, var state: T) { + open class Header(name: String) : Filter(name, 0) + abstract class List(name: String, val values: Array, state: Int = 0) : Filter(name, state) + abstract class Text(name: String, state: String = "") : Filter(name, state) + abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) + abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt new file mode 100644 index 000000000..9a64683c4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.data.source.model + +class FilterList(list: List>) : List> by list { + + constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) + + fun hasSameState(other: FilterList): Boolean { + if (size != other.size) return false + + return (0..lastIndex) + .all { get(it).javaClass == other[it].javaClass && get(it).state == other[it].state } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt index 78b9aa054..a601f93c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt @@ -1,13 +1,3 @@ package eu.kanade.tachiyomi.data.source.model -import eu.kanade.tachiyomi.data.database.models.Manga - -class MangasPage(val page: Int) { - - val mangas: MutableList = mutableListOf() - - lateinit var url: String - - var nextPageUrl: String? = null - -} +data class MangasPage(val mangas: List, val hasNextPage: Boolean) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt new file mode 100644 index 000000000..9bf515a87 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.source.model + +import java.io.Serializable + +interface SChapter : Serializable { + + var url: String + + var name: String + + var date_upload: Long + + var chapter_number: Float + + fun copyFrom(other: SChapter) { + name = other.name + url = other.url + date_upload = other.date_upload + chapter_number = other.chapter_number + } + + companion object { + fun create(): SChapter { + return SChapterImpl() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt new file mode 100644 index 000000000..4995e0f99 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.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 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt new file mode 100644 index 000000000..f7da4ca66 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.data.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 copyFrom(other: SManga) { + 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(): SManga { + return SMangaImpl() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt new file mode 100644 index 000000000..6c4499b40 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.source.model + +class SMangaImpl : SManga { + + 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 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt index 5b8936838..dd1e4af3a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt @@ -1,40 +1,32 @@ package eu.kanade.tachiyomi.data.source.online -import android.net.Uri -import eu.kanade.tachiyomi.data.cache.ChapterCache -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.asObservableSuccess import eu.kanade.tachiyomi.data.network.newCallWithProgress import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.source.Source -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page -import eu.kanade.tachiyomi.util.UrlUtil +import eu.kanade.tachiyomi.data.source.CatalogueSource +import eu.kanade.tachiyomi.data.source.model.* 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 OnlineSource() : Source { +abstract class OnlineSource : CatalogueSource { /** * Network service. */ val network: NetworkHelper by injectLazy() - /** - * Chapter cache. - */ - val chapterCache: ChapterCache by injectLazy() - /** * Preferences helper. */ @@ -46,24 +38,26 @@ abstract class OnlineSource() : Source { abstract val baseUrl: String /** - * An ISO 639-1 compliant language code (two characters in lower case). + * 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. */ - abstract val lang: String + open val versionId = 1 /** - * Whether the source has support for latest updates. + * 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. */ - abstract val supportsLatest: Boolean + 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 by lazy { headersBuilder().build() } - - /** - * Genre filters. - */ - val filters by lazy { getFilterList() } + val headers: Headers by lazy { headersBuilder().build() } /** * Default network client for doing requests. @@ -87,121 +81,88 @@ abstract class OnlineSource() : Source { * Returns an observable containing a page with a list of manga. Normally it's not needed to * override this method. * - * @param page the page object where the information will be saved, like the list of manga, - * the current page and the next page url. + * @param page the page number to retrieve. */ - open fun fetchPopularManga(page: MangasPage): Observable = client - .newCall(popularMangaRequest(page)) - .asObservableSuccess() - .map { response -> - popularMangaParse(response, page) - page - } - - /** - * Returns the request for the popular manga given the page. Override only if it's needed to - * send different headers or request method like POST. - * - * @param page the page object. - */ - open protected fun popularMangaRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = popularMangaInitialUrl() - } - return GET(page.url, headers) + override fun fetchPopularManga(page: Int): Observable { + return client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { response -> + popularMangaParse(response) + } } /** - * Returns the absolute url of the first page to popular manga. + * Returns the request for the popular manga given the page. + * + * @param page the page number to retrieve. */ - abstract protected fun popularMangaInitialUrl(): String + abstract protected fun popularMangaRequest(page: Int): Request /** - * Parse the response from the site. It should add a list of manga and the absolute url to the - * next page (if it has a next one) to [page]. + * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. - * @param page the page object to be filled. */ - abstract protected fun popularMangaParse(response: Response, page: MangasPage) + abstract protected 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 object where the information will be saved, like the list of manga, - * the current page and the next page url. + * @param page the page number to retrieve. * @param query the search query. + * @param filters the list of filters to apply. */ - open fun fetchSearchManga(page: MangasPage, query: String, filters: List>): Observable = client - .newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() - .map { response -> - searchMangaParse(response, page, query, filters) - page - } - - /** - * Returns the request for the search manga given the page. Override only if it's needed to - * send different headers or request method like POST. - * - * @param page the page object. - * @param query the search query. - */ - open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) - } - return GET(page.url, headers) + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchMangaParse(response) + } } /** - * Returns the absolute url of the first page to popular manga. + * 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. */ - abstract protected fun searchMangaInitialUrl(query: String, filters: List>): String + abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request /** - * Parse the response from the site. It should add a list of manga and the absolute url to the - * next page (if it has a next one) to [page]. + * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. - * @param page the page object to be filled. - * @param query the search query. */ - abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) + abstract protected fun searchMangaParse(response: Response): MangasPage /** - * Returns an observable containing a page with a list of latest manga. + * Returns an observable containing a page with a list of latest manga updates. + * + * @param page the page number to retrieve. */ - open fun fetchLatestUpdates(page: MangasPage): Observable = client - .newCall(latestUpdatesRequest(page)) - .asObservableSuccess() - .map { response -> - latestUpdatesParse(response, page) - page - } + override fun fetchLatestUpdates(page: Int): Observable { + 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. */ - open protected fun latestUpdatesRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = latestUpdatesInitialUrl() - } - return GET(page.url, headers) - } + abstract protected fun latestUpdatesRequest(page: Int): Request /** - * Returns the absolute url of the first page to latest manga. + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. */ - abstract protected fun latestUpdatesInitialUrl(): String - - /** - * Same as [popularMangaParse], but for latest manga. - */ - abstract protected fun latestUpdatesParse(response: Response, page: MangasPage) + abstract protected fun latestUpdatesParse(response: Response): MangasPage /** * Returns an observable with the updated details for a manga. Normally it's not needed to @@ -209,33 +170,30 @@ abstract class OnlineSource() : Source { * * @param manga the manga to be updated. */ - override fun fetchMangaDetails(manga: Manga): Observable = client - .newCall(mangaDetailsRequest(manga)) - .asObservableSuccess() - .map { response -> - Manga.create(manga.url, id).apply { - mangaDetailsParse(response, this) - initialized = true + override fun fetchMangaDetails(manga: SManga): Observable { + return client.newCall(mangaDetailsRequest(manga)) + .asObservableSuccess() + .map { response -> + mangaDetailsParse(response).apply { initialized = true } } - } + } /** - * Returns the request for updating a manga. Override only if it's needed to override the url, - * send different headers or request method like POST. + * 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: Manga): Request { + open fun mangaDetailsRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } /** - * Parse the response from the site. It should fill [manga]. + * Parses the response from the site and returns the details of a manga. * * @param response the response from the site. - * @param manga the manga whose fields have to be filled. */ - abstract protected fun mangaDetailsParse(response: Response, manga: Manga) + abstract protected fun mangaDetailsParse(response: Response): SManga /** * Returns an observable with the updated chapter list for a manga. Normally it's not needed to @@ -243,17 +201,13 @@ abstract class OnlineSource() : Source { * * @param manga the manga to look for chapters. */ - override fun fetchChapterList(manga: Manga): Observable> = client - .newCall(chapterListRequest(manga)) - .asObservableSuccess() - .map { response -> - mutableListOf().apply { - chapterListParse(response, this) - if (isEmpty()) { - throw Exception("No chapters found") - } + override fun fetchChapterList(manga: SManga): Observable> { + return client.newCall(chapterListRequest(manga)) + .asObservableSuccess() + .map { response -> + chapterListParse(response) } - } + } /** * Returns the request for updating the chapter list. Override only if it's needed to override @@ -261,68 +215,46 @@ abstract class OnlineSource() : Source { * * @param manga the manga to look for chapters. */ - open protected fun chapterListRequest(manga: Manga): Request { + open protected fun chapterListRequest(manga: SManga): Request { return GET(baseUrl + manga.url, headers) } /** - * Parse the response from the site. It should fill [chapters]. + * Parses the response from the site and returns a list of chapters. * * @param response the response from the site. - * @param chapters the chapter list to be filled. */ - abstract protected fun chapterListParse(response: Response, chapters: MutableList) + abstract protected fun chapterListParse(response: Response): List /** - * Returns an observable with the page list for a chapter. It tries to return the page list from - * the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork]. + * Returns an observable with the page list for a chapter. * * @param chapter the chapter whose page list has to be fetched. */ - final override fun fetchPageList(chapter: Chapter): Observable> = chapterCache - .getPageListFromCache(getChapterCacheKey(chapter)) - .onErrorResumeNext { fetchPageListFromNetwork(chapter) } - - /** - * Returns an observable with the page list for a chapter. Normally it's not needed to override - * this method. - * - * @param chapter the chapter whose page list has to be fetched. - */ - open fun fetchPageListFromNetwork(chapter: Chapter): Observable> = client - .newCall(pageListRequest(chapter)) - .asObservableSuccess() - .map { response -> - mutableListOf().apply { - pageListParse(response, this) - if (isEmpty()) { - throw Exception("Page list is empty") - } + override fun fetchPageList(chapter: SChapter): Observable> { + 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 + * @param chapter the chapter whose page list has to be fetched. */ - open protected fun pageListRequest(chapter: Chapter): Request { + open protected fun pageListRequest(chapter: SChapter): Request { return GET(baseUrl + chapter.url, headers) } /** - * Parse the response from the site. It should fill [pages]. + * Parses the response from the site and returns a list of pages. * * @param response the response from the site. - * @param pages the page list to be filled. */ - abstract protected fun pageListParse(response: Response, pages: MutableList) - - /** - * Returns the key for the page list to be stored in [ChapterCache]. - */ - private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}" + abstract protected fun pageListParse(response: Response): List /** * Returns an observable with the page containing the source url of the image. If there's any @@ -330,16 +262,10 @@ abstract class OnlineSource() : Source { * * @param page the page whose source image has to be fetched. */ - open protected fun fetchImageUrl(page: Page): Observable { - page.status = Page.LOAD_PAGE - return client - .newCall(imageUrlRequest(page)) + open fun fetchImageUrl(page: Page): Observable { + return client.newCall(imageUrlRequest(page)) .asObservableSuccess() .map { imageUrlParse(it) } - .doOnError { page.status = Page.ERROR } - .onErrorReturn { null } - .doOnNext { page.imageUrl = it } - .map { page } } /** @@ -353,31 +279,21 @@ abstract class OnlineSource() : Source { } /** - * Parse the response from the site. It should return the absolute url to the source image. + * Parses the response from the site and returns the absolute url to the source image. * * @param response the response from the site. */ abstract protected fun imageUrlParse(response: Response): String - /** - * Returns an observable of the page with the downloaded image. - * - * @param page the page whose source image has to be downloaded. - */ - final override fun fetchImage(page: Page): Observable = - if (page.imageUrl.isNullOrEmpty()) - fetchImageUrl(page).flatMap { getCachedImage(it) } - else - getCachedImage(page) - /** * Returns an observable with the response of the source image. * * @param page the page whose source image has to be downloaded. */ - fun imageResponse(page: Page): Observable = client - .newCallWithProgress(imageRequest(page), page) - .asObservableSuccess() + 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 @@ -390,68 +306,44 @@ abstract class OnlineSource() : Source { } /** - * Returns an observable of the page that gets the image from the chapter or fallbacks to - * network and copies it to the cache calling [cacheImage]. + * 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 page the page. + * @param url the full url to the chapter. */ - fun getCachedImage(page: Page): Observable { - val imageUrl = page.imageUrl ?: return Observable.just(page) - - return Observable.just(page) - .flatMap { - if (!chapterCache.isImageInCache(imageUrl)) { - cacheImage(page) - } else { - Observable.just(page) - } - } - .doOnNext { - page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl)) - page.status = Page.READY - } - .doOnError { page.status = Page.ERROR } - .onErrorReturn { page } + fun SChapter.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) } /** - * Returns an observable of the page that downloads the image to [ChapterCache]. + * 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 page the page. + * @param url the full url to the manga. */ - private fun cacheImage(page: Page): Observable { - page.status = Page.DOWNLOAD_IMAGE - return imageResponse(page) - .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) } - .map { page } + fun SManga.setUrlWithoutDomain(url: String) { + this.url = getUrlWithoutDomain(url) } - - // Utility methods - - fun fetchAllImageUrlsFromPageList(pages: List) = Observable.from(pages) - .filter { !it.imageUrl.isNullOrEmpty() } - .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) - - fun fetchRemainingImageUrlsFromPageList(pages: List) = Observable.from(pages) - .filter { it.imageUrl.isNullOrEmpty() } - .concatMap { fetchImageUrl(it) } - - fun savePageList(chapter: Chapter, pages: List?) { - if (pages != null) { - chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages) + /** + * Returns the url of the given string without the scheme and domain. + * + * @param orig the full url. + */ + private fun getUrlWithoutDomain(orig: String): String { + try { + val uri = URI(orig) + var out = uri.path + if (uri.query != null) + out += "?" + uri.query + if (uri.fragment != null) + out += "#" + uri.fragment + return out + } catch (e: URISyntaxException) { + return orig } } - fun Chapter.setUrlWithoutDomain(url: String) { - this.url = UrlUtil.getPath(url) - } - - fun Manga.setUrlWithoutDomain(url: String) { - this.url = UrlUtil.getPath(url) - } - - /** * 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]. @@ -459,22 +351,11 @@ abstract class OnlineSource() : Source { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ - open fun prepareNewChapter(chapter: Chapter, manga: Manga) { + open fun prepareNewChapter(chapter: SChapter, manga: SManga) { } - sealed class Filter(val name: String, var state: T) { - open class Header(name: String) : Filter(name, 0) - abstract class List(name: String, val values: Array, state: Int = 0) : Filter(name, state) - abstract class Text(name: String, state: String = "") : Filter(name, state) - abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) - abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { - companion object { - const val STATE_IGNORE = 0 - const val STATE_INCLUDE = 1 - const val STATE_EXCLUDE = 2 - } - } - } - - open fun getFilterList(): List> = emptyList() + /** + * Returns the list of filters for the source. + */ + override fun getFilterList() = FilterList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt new file mode 100644 index 000000000..ce2e14098 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt @@ -0,0 +1,98 @@ +package eu.kanade.tachiyomi.data.source.online + +import android.net.Uri +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.source.model.Page +import rx.Observable +import uy.kohesive.injekt.injectLazy + + +// TODO: this should be handled with a different approach. + +/** + * Chapter cache. + */ +private val chapterCache: ChapterCache by injectLazy() + +/** + * Returns an observable with the page list for a chapter. It tries to return the page list from + * the local cache, otherwise fallbacks to network. + * + * @param chapter the chapter whose page list has to be fetched. + */ +fun OnlineSource.fetchPageListFromCacheThenNet(chapter: Chapter): Observable> { + return chapterCache + .getPageListFromCache(chapter) + .onErrorResumeNext { fetchPageList(chapter) } +} + +/** + * Returns an observable of the page with the downloaded image. + * + * @param page the page whose source image has to be downloaded. + */ +fun OnlineSource.fetchImageFromCacheThenNet(page: Page): Observable { + return if (page.imageUrl.isNullOrEmpty()) + getImageUrl(page).flatMap { getCachedImage(it) } + else + getCachedImage(page) +} + +fun OnlineSource.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 } +} + +/** + * Returns an observable of the page that gets the image from the chapter or fallbacks to + * network and copies it to the cache calling [cacheImage]. + * + * @param page the page. + */ +fun OnlineSource.getCachedImage(page: Page): Observable { + val imageUrl = page.imageUrl ?: return Observable.just(page) + + return Observable.just(page) + .flatMap { + if (!chapterCache.isImageInCache(imageUrl)) { + cacheImage(page) + } else { + Observable.just(page) + } + } + .doOnNext { + page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl)) + page.status = Page.READY + } + .doOnError { page.status = Page.ERROR } + .onErrorReturn { page } +} + +/** + * Returns an observable of the page that downloads the image to [ChapterCache]. + * + * @param page the page. + */ +private fun OnlineSource.cacheImage(page: Page): Observable { + page.status = Page.DOWNLOAD_IMAGE + return fetchImage(page) + .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) } + .map { page } +} + +fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { !it.imageUrl.isNullOrEmpty() } + .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) +} + +fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { + return Observable.from(pages) + .filter { it.imageUrl.isNullOrEmpty() } + .concatMap { getImageUrl(it) } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt index 519c9e3d1..bbacc26e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.data.source.online -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.SChapter +import eu.kanade.tachiyomi.data.source.model.SManga import eu.kanade.tachiyomi.util.asJsoup import okhttp3.Response import org.jsoup.nodes.Document @@ -12,26 +12,25 @@ import org.jsoup.nodes.Element /** * A simple implementation for sources from a website using Jsoup, an HTML parser. */ -abstract class ParsedOnlineSource() : OnlineSource() { +abstract class ParsedOnlineSource : OnlineSource() { /** - * Parse the response from the site and fills [page]. + * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. - * @param page the page object to be filled. */ - override fun popularMangaParse(response: Response, page: MangasPage) { + override fun popularMangaParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(popularMangaSelector())) { - Manga.create(id).apply { - popularMangaFromElement(element, this) - page.mangas.add(this) - } + + val mangas = document.select(popularMangaSelector()).map { element -> + popularMangaFromElement(element) } - popularMangaNextPageSelector()?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") - } + val hasNextPage = popularMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) } /** @@ -40,13 +39,12 @@ abstract class ParsedOnlineSource() : OnlineSource() { abstract protected fun popularMangaSelector(): String /** - * Fills [manga] with the given [element]. Most sites only show the title and the url, it's - * totally safe to fill only those two values. + * 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]. - * @param manga the manga to fill. */ - abstract protected fun popularMangaFromElement(element: Element, manga: Manga) + abstract protected fun popularMangaFromElement(element: Element): SManga /** * Returns the Jsoup selector that returns the tag linking to the next page, or null if @@ -55,24 +53,22 @@ abstract class ParsedOnlineSource() : OnlineSource() { abstract protected fun popularMangaNextPageSelector(): String? /** - * Parse the response from the site and fills [page]. + * Parses the response from the site and returns a [MangasPage] object. * * @param response the response from the site. - * @param page the page object to be filled. - * @param query the search query. */ - override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) { + override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(searchMangaSelector())) { - Manga.create(id).apply { - searchMangaFromElement(element, this) - page.mangas.add(this) - } + + val mangas = document.select(searchMangaSelector()).map { element -> + searchMangaFromElement(element) } - searchMangaNextPageSelector()?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") - } + val hasNextPage = searchMangaNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) } /** @@ -81,13 +77,12 @@ abstract class ParsedOnlineSource() : OnlineSource() { abstract protected fun searchMangaSelector(): String /** - * Fills [manga] with the given [element]. Most sites only show the title and the url, it's - * totally safe to fill only those two values. + * 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]. - * @param manga the manga to fill. */ - abstract protected fun searchMangaFromElement(element: Element, manga: Manga) + abstract protected fun searchMangaFromElement(element: Element): SManga /** * Returns the Jsoup selector that returns the tag linking to the next page, or null if @@ -96,70 +91,67 @@ abstract class ParsedOnlineSource() : OnlineSource() { abstract protected fun searchMangaNextPageSelector(): String? /** - * Parse the response from the site for latest updates and fills [page]. + * Parses the response from the site and returns a [MangasPage] object. + * + * @param response the response from the site. */ - override fun latestUpdatesParse(response: Response, page: MangasPage) { + override fun latestUpdatesParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(latestUpdatesSelector())) { - Manga.create(id).apply { - latestUpdatesFromElement(element, this) - page.mangas.add(this) - } + + val mangas = document.select(latestUpdatesSelector()).map { element -> + latestUpdatesFromElement(element) } - latestUpdatesNextPageSelector()?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") - } + val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> + document.select(selector).first() + } != null + + return MangasPage(mangas, hasNextPage) } /** - * Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates. + * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga. */ abstract protected fun latestUpdatesSelector(): String /** - * Fills [manga] with the given [element]. For latest updates. + * 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]. */ - abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga) + abstract protected fun latestUpdatesFromElement(element: Element): SManga /** - * Returns the Jsoup selector that returns the tag, like [popularMangaNextPageSelector]. + * Returns the Jsoup selector that returns the tag linking to the next page, or null if + * there's no next page. */ abstract protected fun latestUpdatesNextPageSelector(): String? /** - * Parse the response from the site and fills the details of [manga]. + * Parses the response from the site and returns the details of a manga. * * @param response the response from the site. - * @param manga the manga to fill. */ - override fun mangaDetailsParse(response: Response, manga: Manga) { - mangaDetailsParse(response.asJsoup(), manga) + override fun mangaDetailsParse(response: Response): SManga { + return mangaDetailsParse(response.asJsoup()) } /** - * Fills the details of [manga] from the given [document]. + * Returns the details of the manga from the given [document]. * * @param document the parsed document. - * @param manga the manga to fill. */ - abstract protected fun mangaDetailsParse(document: Document, manga: Manga) + abstract protected fun mangaDetailsParse(document: Document): SManga /** - * Parse the response from the site and fills the chapter list. + * Parses the response from the site and returns a list of chapters. * * @param response the response from the site. - * @param chapters the list of chapters to fill. */ - override fun chapterListParse(response: Response, chapters: MutableList) { + override fun chapterListParse(response: Response): List { val document = response.asJsoup() - - for (element in document.select(chapterListSelector())) { - Chapter.create().apply { - chapterFromElement(element, this) - chapters.add(this) - } - } + return document.select(chapterListSelector()).map { chapterFromElement(it) } } /** @@ -168,30 +160,27 @@ abstract class ParsedOnlineSource() : OnlineSource() { abstract protected fun chapterListSelector(): String /** - * Fills [chapter] with the given [element]. + * Returns a chapter from the given element. * * @param element an element obtained from [chapterListSelector]. - * @param chapter the chapter to fill. */ - abstract protected fun chapterFromElement(element: Element, chapter: Chapter) + abstract protected fun chapterFromElement(element: Element): SChapter /** - * Parse the response from the site and fills the page list. + * Parses the response from the site and returns the page list. * * @param response the response from the site. - * @param pages the list of pages to fill. */ - override fun pageListParse(response: Response, pages: MutableList) { - pageListParse(response.asJsoup(), pages) + override fun pageListParse(response: Response): List { + return pageListParse(response.asJsoup()) } /** - * Fills [pages] from the given [document]. + * Returns a page list from the given document. * * @param document the parsed document. - * @param pages the list of pages to fill. */ - abstract protected fun pageListParse(document: Document, pages: MutableList) + abstract protected fun pageListParse(document: Document): List /** * Parse the response from the site and returns the absolute url to the source image. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt index 85efe6412..c5e7789ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt @@ -1,11 +1,8 @@ package eu.kanade.tachiyomi.data.source.online -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.attrOrText import okhttp3.Request @@ -36,92 +33,108 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() { } override val id = map.id.let { - if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff + (it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong() } - override fun popularMangaRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = popularMangaInitialUrl() + // Ugly, but needed after the changes + var popularNextPage: String? = null + var searchNextPage: String? = null + var latestNextPage: String? = null + + override fun popularMangaRequest(page: Int): Request { + val url = if (page == 1) { + popularNextPage = null + map.popular.url + } else { + popularNextPage!! } return when (map.popular.method?.toLowerCase()) { - "post" -> POST(page.url, headers, map.popular.createForm()) - else -> GET(page.url, headers) + "post" -> POST(url, headers, map.popular.createForm()) + else -> GET(url, headers) } } - override fun popularMangaInitialUrl() = map.popular.url - - override fun popularMangaParse(response: Response, page: MangasPage) { + override fun popularMangaParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(map.popular.manga_css)) { - Manga.create(id).apply { + + val mangas = document.select(map.popular.manga_css).map { element -> + SManga.create().apply { title = element.text() setUrlWithoutDomain(element.attr("href")) - page.mangas.add(this) } } - map.popular.next_url_css?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") + popularNextPage = map.popular.next_url_css?.let { selector -> + document.select(selector).first()?.absUrl("href") } + + return MangasPage(mangas, popularNextPage != null) } - override fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if (page == 1) { + searchNextPage = null + map.search.url.replace("\$query", query) + } else { + searchNextPage!! } return when (map.search.method?.toLowerCase()) { - "post" -> POST(page.url, headers, map.search.createForm()) - else -> GET(page.url, headers) + "post" -> POST(url, headers, map.search.createForm()) + else -> GET(url, headers) } } - override fun searchMangaInitialUrl(query: String, filters: List>) = map.search.url.replace("\$query", query) - - override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) { + override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(map.search.manga_css)) { - Manga.create(id).apply { + + val mangas = document.select(map.search.manga_css).map { element -> + SManga.create().apply { title = element.text() setUrlWithoutDomain(element.attr("href")) - page.mangas.add(this) } } - map.search.next_url_css?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") + searchNextPage = map.search.next_url_css?.let { selector -> + document.select(selector).first()?.absUrl("href") } + + return MangasPage(mangas, searchNextPage != null) } - override fun latestUpdatesRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = latestUpdatesInitialUrl() + override fun latestUpdatesRequest(page: Int): Request { + val url = if (page == 1) { + latestNextPage = null + map.latestupdates!!.url + } else { + latestNextPage!! } return when (map.latestupdates!!.method?.toLowerCase()) { - "post" -> POST(page.url, headers, map.latestupdates.createForm()) - else -> GET(page.url, headers) + "post" -> POST(url, headers, map.latestupdates.createForm()) + else -> GET(url, headers) } } - override fun latestUpdatesInitialUrl() = map.latestupdates!!.url - - override fun latestUpdatesParse(response: Response, page: MangasPage) { + override fun latestUpdatesParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(map.latestupdates!!.manga_css)) { - Manga.create(id).apply { + + val mangas = document.select(map.latestupdates!!.manga_css).map { element -> + SManga.create().apply { title = element.text() setUrlWithoutDomain(element.attr("href")) - page.mangas.add(this) } } - map.latestupdates.next_url_css?.let { selector -> - page.nextPageUrl = document.select(selector).first()?.absUrl("href") + popularNextPage = map.latestupdates.next_url_css?.let { selector -> + document.select(selector).first()?.absUrl("href") } + + return MangasPage(mangas, popularNextPage != null) } - override fun mangaDetailsParse(response: Response, manga: Manga) { + override fun mangaDetailsParse(response: Response): SManga { val document = response.asJsoup() + + val manga = SManga.create() with(map.manga) { val pool = parts.get(document) @@ -130,18 +143,21 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() { manga.description = summary?.process(document, pool) manga.thumbnail_url = cover?.process(document, pool) manga.genre = genres?.process(document, pool) - manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN + manga.status = status?.getStatus(document, pool) ?: SManga.UNKNOWN } + return manga } - override fun chapterListParse(response: Response, chapters: MutableList) { + override fun chapterListParse(response: Response): List { val document = response.asJsoup() + + val chapters = mutableListOf() with(map.chapters) { val pool = emptyMap() val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH) for (element in document.select(chapter_css)) { - val chapter = Chapter.create() + val chapter = SChapter.create() element.select(title).first().let { chapter.name = it.text() chapter.setUrlWithoutDomain(it.attr("href")) @@ -151,12 +167,15 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() { chapters.add(chapter) } } + return chapters } - override fun pageListParse(response: Response, pages: MutableList) { + override fun pageListParse(response: Response): List { val body = response.body().string() val url = response.request().url().toString() + val pages = mutableListOf() + // TODO lazy initialization in Kotlin 1.1 val document = Jsoup.parse(body, url) @@ -194,6 +213,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() { page.imageUrl = url } } + return pages } override fun imageUrlParse(response: Response): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt index e4b643481..b3c0a3bbb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.source.online -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.source.model.SManga import okhttp3.FormBody import okhttp3.RequestBody import org.jsoup.nodes.Document @@ -164,15 +164,15 @@ class StatusNode(private val map: Map) : SelectableNode(map) { fun getStatus(document: Element, cache: Map): Int { val text = process(document, cache) complete?.let { - if (text.contains(it)) return Manga.COMPLETED + if (text.contains(it)) return SManga.COMPLETED } ongoing?.let { - if (text.contains(it)) return Manga.ONGOING + if (text.contains(it)) return SManga.ONGOING } licensed?.let { - if (text.contains(it)) return Manga.LICENSED + if (text.contains(it)) return SManga.LICENSED } - return Manga.UNKNOWN + return SManga.UNKNOWN } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt index fa19cf950..6f4772752 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt @@ -1,13 +1,10 @@ package eu.kanade.tachiyomi.data.source.online.english import android.text.Html -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.asObservable -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.util.asJsoup @@ -25,7 +22,9 @@ import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern -class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { +class Batoto : ParsedOnlineSource(), LoginSource { + + override val id: Long = 1 override val name = "Batoto" @@ -56,70 +55,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { .add("Referer", "http://bato.to/reader") .build() - override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1" - - override fun latestUpdatesInitialUrl() = "$baseUrl/search_ajax?order_cond=update&order=desc&p=1" - - override fun popularMangaParse(response: Response, page: MangasPage) { - val document = response.asJsoup() - for (element in document.select(popularMangaSelector())) { - Manga.create(id).apply { - popularMangaFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let { - "$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}" - } + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers) } - override fun latestUpdatesParse(response: Response, page: MangasPage) { - val document = response.asJsoup() - for (element in document.select(latestUpdatesSelector())) { - Manga.create(id).apply { - latestUpdatesFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = document.select(latestUpdatesNextPageSelector()).first()?.let { - "$baseUrl/search_ajax?order_cond=update&order=desc&p=${page.page + 1}" - } + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers) } override fun popularMangaSelector() = "tr:has(a)" override fun latestUpdatesSelector() = "tr:has(a)" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a[href^=http://bato.to]").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text().trim() } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "#show_more_row" override fun latestUpdatesNextPageSelector() = "#show_more_row" - override fun searchMangaInitialUrl(query: String, filters: List>) = searchMangaUrl(query, filters, 1) - - private fun searchMangaUrl(query: String, filterStates: List>, page: Int): String { + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder() if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c") var genres = "" - for (filter in if (filterStates.isEmpty()) filters else filterStates) { + filters.forEach { filter -> when (filter) { - is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) { - url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c") + is Status -> if (!filter.isIgnored()) { + url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c") } - is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) { - genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id + is Genre -> if (!filter.isIgnored()) { + genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id } is TextField -> { if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state) @@ -136,89 +111,67 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { } if (!genres.isEmpty()) url.addQueryParameter("genres", genres) url.addQueryParameter("p", page.toString()) - return url.toString() - } - - override fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) - } - return GET(page.url, headers) - } - - override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) { - val document = response.asJsoup() - for (element in document.select(searchMangaSelector())) { - Manga.create(id).apply { - searchMangaFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let { - searchMangaUrl(query, filters, page.page + 1) - } + return GET(url.toString(), headers) } override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() - override fun mangaDetailsRequest(manga: Manga): Request { + override fun mangaDetailsRequest(manga: SManga): Request { val mangaId = manga.url.substringAfterLast("r") return GET("$baseUrl/comic_pop?id=$mangaId", headers) } - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val tbody = document.select("tbody").first() val artistElement = tbody.select("tr:contains(Author/Artist:)").first() + val manga = SManga.create() manga.author = artistElement.selectText("td:eq(1)") manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)") manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src") manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)")) manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ") + return manga } private fun parseStatus(status: String?) = when (status) { - "Ongoing" -> Manga.ONGOING - "Complete" -> Manga.COMPLETED - else -> Manga.UNKNOWN + "Ongoing" -> SManga.ONGOING + "Complete" -> SManga.COMPLETED + else -> SManga.UNKNOWN } - override fun chapterListParse(response: Response, chapters: MutableList) { + override fun chapterListParse(response: Response): List { val body = response.body().string() val matcher = staffNotice.matcher(body) if (matcher.find()) { + @Suppress("DEPRECATION") val notice = Html.fromHtml(matcher.group(1)).toString().trim() throw Exception(notice) } val document = response.asJsoup(body) - - for (element in document.select(chapterListSelector())) { - Chapter.create().apply { - chapterFromElement(element, this) - chapters.add(this) - } - } + return document.select(chapterListSelector()).map { chapterFromElement(it) } } override fun chapterListSelector() = "tr.row.lang_English.chapter_row" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a[href^=http://bato.to/reader").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() chapter.date_upload = element.select("td").getOrNull(4)?.let { parseDateFromElement(it) } ?: 0 + return chapter } private fun parseDateFromElement(dateElement: Element): Long { @@ -246,12 +199,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { return date.time } - override fun pageListRequest(chapter: Chapter): Request { + override fun pageListRequest(chapter: SChapter): Request { val id = chapter.url.substringAfterLast("#") return GET("$baseUrl/areader?id=$id&p=1", pageHeaders) } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + val pages = mutableListOf() val selectElement = document.select("#page_select").first() if (selectElement != null) { for ((i, element) in selectElement.select("option").withIndex()) { @@ -264,6 +218,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { pages.add(Page(i, "", element.attr("src"))) } } + return pages } override fun imageUrlRequest(page: Page): Request { @@ -308,7 +263,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" } } - override fun fetchChapterList(manga: Manga): Observable> { + override fun fetchChapterList(manga: SManga): Observable> { if (!isLogged()) { val username = preferences.sourceUsername(this) val password = preferences.sourcePassword(this) @@ -328,7 +283,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { override fun toString(): String = name } - private class Status() : Filter.TriState("Completed") + private class Status : Filter.TriState("Completed") private class Genre(name: String, val id: Int) : Filter.TriState(name) private class TextField(name: String, val key: String) : Filter.Text(name) private class ListField(name: String, val key: String, values: Array, state: Int = 0) : Filter.List(name, values, state) @@ -338,7 +293,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource { // const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})` // }).join(',\n') // on https://bato.to/search - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( TextField("Author", "artist_name"), ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))), Status(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt index 1068f6efb..be38bc53a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt @@ -1,11 +1,8 @@ package eu.kanade.tachiyomi.data.source.online.english -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import okhttp3.FormBody import okhttp3.OkHttpClient @@ -16,7 +13,9 @@ import org.jsoup.nodes.Element import java.text.SimpleDateFormat import java.util.regex.Pattern -class Kissmanga(override val id: Int) : ParsedOnlineSource() { +class Kissmanga : ParsedOnlineSource() { + + override val id: Long = 4 override val name = "Kissmanga" @@ -28,38 +27,40 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() { override val client: OkHttpClient = network.cloudflareClient - override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular" - - override fun latestUpdatesInitialUrl() = "http://kissmanga.com/MangaList/LatestUpdate" - override fun popularMangaSelector() = "table.listing tr:gt(1)" override fun latestUpdatesSelector() = "table.listing tr:gt(1)" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/MangaList/MostPopular?page=$page", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("td a:eq(0)").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "li > a:contains(› Next)" override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)" - override fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) - } - + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val form = FormBody.Builder().apply { add("mangaName", query) - for (filter in if (filters.isEmpty()) this@Kissmanga.filters else filters) { + for (filter in if (filters.isEmpty()) getFilterList() else filters) { when (filter) { is Author -> add("authorArtist", filter.state) is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state]) @@ -67,50 +68,53 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() { } } } - return POST(page.url, headers, form.build()) + return POST("$baseUrl/AdvanceSearch", headers, form.build()) } - override fun searchMangaInitialUrl(query: String, filters: List>) = "$baseUrl/AdvanceSearch" - override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun searchMangaNextPageSelector() = null - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val infoElement = document.select("div.barContent").first() + val manga = SManga.create() manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text() manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text() manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text() manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src") + return manga } fun parseStatus(status: String) = when { - status.contains("Ongoing") -> Manga.ONGOING - status.contains("Completed") -> Manga.COMPLETED - else -> Manga.UNKNOWN + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN } override fun chapterListSelector() = "table.listing tr:gt(1)" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { SimpleDateFormat("MM/dd/yyyy").parse(it).time } ?: 0 + return chapter } - override fun pageListRequest(chapter: Chapter) = POST(baseUrl + chapter.url, headers) + override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers) - override fun pageListParse(response: Response, pages: MutableList) { + override fun pageListParse(response: Response): List { + val pages = mutableListOf() //language=RegExp val p = Pattern.compile("""lstImages.push\("(.+?)"""") val m = p.matcher(response.body().string()) @@ -119,10 +123,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() { while (m.find()) { pages.add(Page(i++, "", m.group(1))) } + return pages } - // Not used - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + throw Exception("Not used") } override fun imageUrlRequest(page: Page) = GET(page.url) @@ -131,57 +136,58 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() { private class Status() : Filter.TriState("Completed") private class Author() : Filter.Text("Author") - private class Genre(name: String, val id: Int) : Filter.TriState(name) + private class Genre(name: String) : Filter.TriState(name) // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n') // on http://kissmanga.com/AdvanceSearch - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( Author(), Status(), Filter.Header("Genres"), - Genre("Action", 0), - Genre("Adult", 1), - Genre("Adventure", 2), - Genre("Comedy", 3), - Genre("Comic", 4), - Genre("Cooking", 5), - Genre("Doujinshi", 6), - Genre("Drama", 7), - Genre("Ecchi", 8), - Genre("Fantasy", 9), - Genre("Gender Bender", 10), - Genre("Harem", 11), - Genre("Historical", 12), - Genre("Horror", 13), - Genre("Josei", 14), - Genre("Lolicon", 15), - Genre("Manga", 16), - Genre("Manhua", 17), - Genre("Manhwa", 18), - Genre("Martial Arts", 19), - Genre("Mature", 20), - Genre("Mecha", 21), - Genre("Medical", 22), - Genre("Music", 23), - Genre("Mystery", 24), - Genre("One shot", 25), - Genre("Psychological", 26), - Genre("Romance", 27), - Genre("School Life", 28), - Genre("Sci-fi", 29), - Genre("Seinen", 30), - Genre("Shotacon", 31), - Genre("Shoujo", 32), - Genre("Shoujo Ai", 33), - Genre("Shounen", 34), - Genre("Shounen Ai", 35), - Genre("Slice of Life", 36), - Genre("Smut", 37), - Genre("Sports", 38), - Genre("Supernatural", 39), - Genre("Tragedy", 40), - Genre("Webtoon", 41), - Genre("Yaoi", 42), - Genre("Yuri", 43) + Genre("4-Koma"), + Genre("Action"), + Genre("Adult"), + Genre("Adventure"), + Genre("Comedy"), + Genre("Comic"), + Genre("Cooking"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Lolicon"), + Genre("Manga"), + Genre("Manhua"), + Genre("Manhwa"), + Genre("Martial Arts"), + Genre("Mature"), + Genre("Mecha"), + Genre("Medical"), + Genre("Music"), + Genre("Mystery"), + Genre("One shot"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shotacon"), + Genre("Shoujo"), + Genre("Shoujo Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Smut"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Webtoon"), + Genre("Yaoi"), + Genre("Yuri") ) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt index 70e2ce334..7f2060eb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt @@ -1,19 +1,19 @@ package eu.kanade.tachiyomi.data.source.online.english -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource -import eu.kanade.tachiyomi.util.asJsoup import okhttp3.HttpUrl -import okhttp3.Response +import okhttp3.Request import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.ParseException import java.text.SimpleDateFormat import java.util.* -class Mangafox(override val id: Int) : ParsedOnlineSource() { +class Mangafox : ParsedOnlineSource() { + + override val id: Long = 3 override val name = "Mangafox" @@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/directory/" - - override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?latest" - override fun popularMangaSelector() = "div#mangalist > ul.list > li" + + override fun popularMangaRequest(page: Int): Request { + val pageStr = if (page != 1) "$page.htm" else "" + return GET("$baseUrl/directory/$pageStr", headers) + } override fun latestUpdatesSelector() = "div#mangalist > ul.list > li" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun latestUpdatesRequest(page: Int): Request { + val pageStr = if (page != 1) "$page.htm" else "" + return GET("$baseUrl/directory/$pageStr?latest") + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a.title").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "a:has(span.next)" override fun latestUpdatesNextPageSelector() = "a:has(span.next)" - override fun searchMangaInitialUrl(query: String, filters: List>): String { + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) - for (filter in if (filters.isEmpty()) this@Mangafox.filters else filters) { + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> when (filter) { is Genre -> url.addQueryParameter(filter.id, filter.state.toString()) is TextField -> url.addQueryParameter(filter.key, filter.state) @@ -56,47 +64,54 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() { is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za") } } - return url.toString() + url.addQueryParameter("page", page.toString()) + return GET(url.toString(), headers) } override fun searchMangaSelector() = "div#mangalist > ul.list > li" - override fun searchMangaFromElement(element: Element, manga: Manga) { + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a.title").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } override fun searchMangaNextPageSelector() = "a:has(span.next)" - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val infoElement = document.select("div#title").first() val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() val sideInfoElement = document.select("#series_info").first() + val manga = SManga.create() manga.author = rowElement.select("td:eq(1)").first()?.text() manga.artist = rowElement.select("td:eq(2)").first()?.text() manga.genre = rowElement.select("td:eq(3)").first()?.text() manga.description = infoElement.select("p.summary").first()?.text() manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") + return manga } private fun parseStatus(status: String) = when { - status.contains("Ongoing") -> Manga.ONGOING - status.contains("Completed") -> Manga.COMPLETED - else -> Manga.UNKNOWN + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN } override fun chapterListSelector() = "div#chapters li div" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a.tips").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter } private fun parseChapterDate(date: String): Long { @@ -124,17 +139,14 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() { } } - override fun pageListParse(response: Response, pages: MutableList) { - val document = response.asJsoup() + override fun pageListParse(document: Document): List { + val url = document.baseUri().substringBeforeLast('/') - val url = response.request().url().toString().substringBeforeLast('/') + val pages = mutableListOf() document.select("select.m").first()?.select("option:not([value=0])")?.forEach { pages.add(Page(pages.size, "$url/${it.attr("value")}.html")) } - } - - // Not used, overrides parent. - override fun pageListParse(document: Document, pages: MutableList) { + return pages } override fun imageUrlParse(document: Document): String { @@ -157,7 +169,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() { // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n') // on http://mangafox.me/search.php - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( TextField("Author", "author"), TextField("Artist", "artist"), ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt index b4c683883..4280a9a3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt @@ -1,17 +1,19 @@ package eu.kanade.tachiyomi.data.source.online.english -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import okhttp3.HttpUrl +import okhttp3.Request import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.ParseException import java.text.SimpleDateFormat import java.util.* -class Mangahere(override val id: Int) : ParsedOnlineSource() { +class Mangahere : ParsedOnlineSource() { + + override val id: Long = 2 override val name = "Mangahere" @@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za" - - override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za" - override fun popularMangaSelector() = "div.directory_list > ul > li" override fun latestUpdatesSelector() = "div.directory_list > ul > li" - private fun mangaFromElement(query: String, element: Element, manga: Manga) { + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/directory/$page.htm?views.za", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers) + } + + private fun mangaFromElement(query: String, element: Element): SManga { + val manga = SManga.create() element.select(query).first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text() } + return manga } - override fun popularMangaFromElement(element: Element, manga: Manga) { - mangaFromElement("div.title > a", element, manga) + override fun popularMangaFromElement(element: Element): SManga { + return mangaFromElement("div.title > a", element) } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "div.next-page > a.next" override fun latestUpdatesNextPageSelector() = "div.next-page > a.next" - override fun searchMangaInitialUrl(query: String, filters: List>): String { + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) - for (filter in if (filters.isEmpty()) this@Mangahere.filters else filters) { + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> when (filter) { is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state]) is Genre -> url.addQueryParameter(filter.id, filter.state.toString()) @@ -59,39 +67,41 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() { is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za") } } - return url.toString() + url.addQueryParameter("page", page.toString()) + return GET(url.toString(), headers) } - override fun searchMangaSelector() = "div.result_search > dl:has(dt)" - override fun searchMangaFromElement(element: Element, manga: Manga) { - mangaFromElement("a.manga_info", element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return mangaFromElement("a.manga_info", element) } override fun searchMangaNextPageSelector() = "div.next-page > a.next" - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val detailElement = document.select(".manga_detail_top").first() val infoElement = detailElement.select(".detail_topText").first() + val manga = SManga.create() manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src") + return manga } private fun parseStatus(status: String) = when { - status.contains("Ongoing") -> Manga.ONGOING - status.contains("Completed") -> Manga.COMPLETED - else -> Manga.UNKNOWN + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN } override fun chapterListSelector() = ".detail_list > ul:not([class]) > li" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val parentEl = element.select("span.left").first() val urlElement = parentEl.select("a").first() @@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() { title = " - " + title } + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() + volume + title chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter } private fun parseChapterDate(date: String): Long { @@ -136,11 +148,13 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() { } } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + val pages = mutableListOf() document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { pages.add(Page(pages.size, it.attr("value"))) } pages.getOrNull(0)?.imageUrl = imageUrlParse(document) + return pages } override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src") @@ -157,7 +171,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() { // [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n') // http://www.mangahere.co/advsearch.htm - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( TextField("Author", "author"), TextField("Artist", "artist"), ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt index c4e56d639..c95c258fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt @@ -1,22 +1,19 @@ package eu.kanade.tachiyomi.data.source.online.english -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource -import eu.kanade.tachiyomi.util.asJsoup import okhttp3.FormBody import okhttp3.HttpUrl import okhttp3.Request -import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.SimpleDateFormat import java.util.regex.Pattern -class Mangasee(override val id: Int) : ParsedOnlineSource() { +class Mangasee : ParsedOnlineSource() { + + override val id: Long = 9 override val name = "Mangasee" @@ -30,46 +27,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { private val indexPattern = Pattern.compile("-index-(.*?)-") - override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1" - override fun popularMangaSelector() = "div.requested > div.row" - override fun popularMangaRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = popularMangaInitialUrl() - } - val (body, requestUrl) = convertQueryToPost(page) + override fun popularMangaRequest(page: Int): Request { + val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1") return POST(requestUrl, headers, body.build()) } - override fun popularMangaParse(response: Response, page: MangasPage) { - val document = response.asJsoup() - for (element in document.select(popularMangaSelector())) { - Manga.create(id).apply { - popularMangaFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = page.url - } - - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a.resultLink").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - // Not used, overrides parent. - override fun popularMangaNextPageSelector() = "" + override fun popularMangaNextPageSelector() = "button.requestMore" - override fun searchMangaInitialUrl(query: String, filters: List>): String { + override fun searchMangaSelector() = "div.requested > div.row" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder() if (!query.isEmpty()) url.addQueryParameter("keyword", query) var genres: String? = null var genresNo: String? = null - for (filter in if (filters.isEmpty()) this@Mangasee.filters else filters) { + for (filter in if (filters.isEmpty()) getFilterList() else filters) { when (filter) { is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s -> url.addQueryParameter(s, filter.values[filter.state].values[i]) @@ -84,22 +67,14 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { } if (genres != null) url.addQueryParameter("genre", genres) if (genresNo != null) url.addQueryParameter("genreNo", genresNo) - return url.toString() - } - override fun searchMangaSelector() = "div.searchResults > div.requested > div.row" - - override fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) - } - val (body, requestUrl) = convertQueryToPost(page) + val (body, requestUrl) = convertQueryToPost(page, url.toString()) return POST(requestUrl, headers, body.build()) } - private fun convertQueryToPost(page: MangasPage): Pair { - val url = HttpUrl.parse(page.url) - val body = FormBody.Builder().add("page", page.page.toString()) + private fun convertQueryToPost(page: Int, url: String): Pair { + val url = HttpUrl.parse(url) + val body = FormBody.Builder().add("page", page.toString()) for (i in 0..url.querySize() - 1) { body.add(url.queryParameterName(i), url.queryParameterValue(i)) } @@ -107,63 +82,57 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { return Pair(body, requestUrl) } - override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) { - val document = response.asJsoup() - for (element in document.select(popularMangaSelector())) { - Manga.create(id).apply { - popularMangaFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = page.url - } - - override fun searchMangaFromElement(element: Element, manga: Manga) { + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a.resultLink").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - // Not used, overrides parent. - override fun searchMangaNextPageSelector() = "" + override fun searchMangaNextPageSelector() = "button.requestMore" - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val detailElement = document.select("div.well > div.row").first() + val manga = SManga.create() manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text() manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString() manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text() manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src") + return manga } private fun parseStatus(status: String) = when { - status.contains("Ongoing (Scan)") -> Manga.ONGOING - status.contains("Complete (Scan)") -> Manga.COMPLETED - else -> Manga.UNKNOWN + status.contains("Ongoing (Scan)") -> SManga.ONGOING + status.contains("Complete (Scan)") -> SManga.COMPLETED + else -> SManga.UNKNOWN } override fun chapterListSelector() = "div.chapter-list > a" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: "" chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0 + return chapter } private fun parseChapterDate(dateAsString: String): Long { return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time } - override fun pageListParse(response: Response, pages: MutableList) { - val document = response.asJsoup() - val fullUrl = response.request().url().toString() + override fun pageListParse(document: Document): List { + val fullUrl = document.baseUri() val url = fullUrl.substringBeforeLast('/') + val pages = mutableListOf() + val series = document.select("input.IndexName").first().attr("value") val chapter = document.select("span.CurChapter").first().text() var index = "" @@ -178,10 +147,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html")) } pages.getOrNull(0)?.imageUrl = imageUrlParse(document) - } - - // Not used, overrides parent. - override fun pageListParse(document: Document, pages: MutableList) { + return pages } override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src") @@ -197,7 +163,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n') // http://mangasee.co/advanced-search/ - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( TextField("Years", "year"), TextField("Author", "author"), Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()), @@ -249,34 +215,18 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { Genre("Yuri") ) - override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php" - - // Not used, overrides parent. - override fun latestUpdatesNextPageSelector(): String = "" + override fun latestUpdatesNextPageSelector() = "button.requestMore" override fun latestUpdatesSelector(): String = "a.latestSeries" - override fun latestUpdatesRequest(page: MangasPage): Request { - if (page.page == 1) { - page.url = latestUpdatesInitialUrl() - } - val (body, requestUrl) = convertQueryToPost(page) + override fun latestUpdatesRequest(page: Int): Request { + val url = "http://mangaseeonline.net/home/latest.request.php" + val (body, requestUrl) = convertQueryToPost(page, url) return POST(requestUrl, headers, body.build()) } - override fun latestUpdatesParse(response: Response, page: MangasPage) { - val document = response.asJsoup() - for (element in document.select(latestUpdatesSelector())) { - Manga.create(id).apply { - latestUpdatesFromElement(element, this) - page.mangas.add(this) - } - } - - page.nextPageUrl = page.url - } - - override fun latestUpdatesFromElement(element: Element, manga: Manga) { + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a.latestSeries").first().let { val chapterUrl = it.attr("href") val indexOfMangaUrl = chapterUrl.indexOf("-chapter-") @@ -288,6 +238,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() { manga.setUrlWithoutDomain("/manga" + mangaUrl) manga.title = title } + return manga } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt index 09b520d14..fae5bf4da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt @@ -1,10 +1,8 @@ package eu.kanade.tachiyomi.data.source.online.english -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.POST -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import okhttp3.Headers import okhttp3.OkHttpClient @@ -13,7 +11,9 @@ import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.util.* -class Readmangatoday(override val id: Int) : ParsedOnlineSource() { +class Readmangatoday : ParsedOnlineSource() { + + override val id: Long = 8 override val name = "ReadMangaToday" @@ -33,41 +33,39 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() { add("X-Requested-With", "XMLHttpRequest") } - override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/" + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/hot-manga/$page", headers) + } - override fun latestUpdatesInitialUrl() = "$baseUrl/latest-releases/" + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/latest-releases/$page", headers) + } override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box" override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("div.title > h2 > a").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.attr("title") } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" - override fun latestUpdatesNextPageSelector(): String = "div.hot-manga > ul.pagination > li > a:contains(»)" - - override fun searchMangaInitialUrl(query: String, filters: List>) = - "$baseUrl/service/advanced_search" - - - override fun searchMangaRequest(page: MangasPage, query: String, filters: List>): Request { - if (page.page == 1) { - page.url = searchMangaInitialUrl(query, filters) - } + override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val builder = okhttp3.FormBody.Builder() builder.add("manga-name", query) - for (filter in if (filters.isEmpty()) this@Readmangatoday.filters else filters) { + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> when (filter) { is TextField -> builder.add(filter.key, filter.state) is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state]) @@ -75,49 +73,54 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() { is Genre -> when (filter.state) { Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString()) Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", filter.id.toString()) - } } } - return POST(page.url, headers, builder.build()) + return POST("$baseUrl/service/advanced_search", headers, builder.build()) } override fun searchMangaSelector() = "div.style-list > div.box" - override fun searchMangaFromElement(element: Element, manga: Manga) { + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("div.title > h2 > a").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.attr("title") } + return manga } override fun searchMangaNextPageSelector() = "div.next-page > a.next" - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val detailElement = document.select("div.movie-meta").first() + val manga = SManga.create() manga.author = document.select("ul.cast-list li.director > ul a").first()?.text() manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text() manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text() manga.description = detailElement.select("li.movie-detail").first()?.text() manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src") + return manga } private fun parseStatus(status: String) = when { - status.contains("Ongoing") -> Manga.ONGOING - status.contains("Completed") -> Manga.COMPLETED - else -> Manga.UNKNOWN + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN } override fun chapterListSelector() = "ul.chp_lst > li" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.select("span.val").text() chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter } private fun parseChapterDate(date: String): Long { @@ -125,7 +128,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() { if (dateWords.size == 3) { val timeAgo = Integer.parseInt(dateWords[0]) - var date: Calendar = Calendar.getInstance() + val date: Calendar = Calendar.getInstance() if (dateWords[1].contains("Minute")) { date.add(Calendar.MINUTE, -timeAgo) @@ -141,17 +144,19 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() { date.add(Calendar.YEAR, -timeAgo) } - return date.getTimeInMillis() + return date.timeInMillis } return 0L } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + val pages = mutableListOf() document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach { pages.add(Page(pages.size, it.attr("value"))) } pages.getOrNull(0)?.imageUrl = imageUrlParse(document) + return pages } override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") @@ -163,7 +168,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() { // [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n') // http://www.readmanga.today/advanced-search - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( TextField("Author", "author-name"), TextField("Artist", "artist-name"), Type(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt index 25c3dcf6a..3e0ae6c62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt @@ -1,16 +1,19 @@ package eu.kanade.tachiyomi.data.source.online.german -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.FilterList import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.model.SChapter +import eu.kanade.tachiyomi.data.source.model.SManga import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.Response +import okhttp3.Request import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.SimpleDateFormat -class WieManga(override val id: Int) : ParsedOnlineSource() { +class WieManga : ParsedOnlineSource() { + + override val id: Long = 10 override val name = "Wie Manga!" @@ -20,50 +23,61 @@ class WieManga(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/" - - override fun latestUpdatesInitialUrl() = "$baseUrl/list/New-Update/" - override fun popularMangaSelector() = ".booklist td > div" override fun latestUpdatesSelector() = ".booklist td > div" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list/Hot-Book/", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list/New-Update/", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { val image = element.select("dt img") val title = element.select("dd a:first-child") + val manga = SManga.create() manga.setUrlWithoutDomain(title.attr("href")) manga.title = title.text() manga.thumbnail_url = image.attr("src") + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = null override fun latestUpdatesNextPageSelector() = null - override fun searchMangaInitialUrl(query: String, filters: List>) = "$baseUrl/search/?wd=$query" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/search/?wd=$query", headers) + } override fun searchMangaSelector() = ".searchresult td > div" - override fun searchMangaFromElement(element: Element, manga: Manga) { + override fun searchMangaFromElement(element: Element): SManga { val image = element.select(".resultimg img") val title = element.select(".resultbookname") + val manga = SManga.create() manga.setUrlWithoutDomain(title.attr("href")) manga.title = title.text() manga.thumbnail_url = image.attr("src") + return manga } override fun searchMangaNextPageSelector() = ".pagetor a.l" - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first() val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first() + val manga = SManga.create() manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text() manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text() manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "") @@ -74,32 +88,33 @@ class WieManga(override val id: Int) : ParsedOnlineSource() { if (manga.artist == "RSS") manga.artist = null + return manga } override fun chapterListSelector() = ".chapterlist tr:not(:first-child)" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select(".col1 a").first() val dateElement = element.select(".col3 a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter } private fun parseChapterDate(date: String): Long { return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time } - override fun pageListParse(response: Response, pages: MutableList) { - val document = response.asJsoup() + override fun pageListParse(document: Document): List { + val pages = mutableListOf() document.select("select#page").first().select("option").forEach { pages.add(Page(pages.size, it.attr("value"))) } - } - - override fun pageListParse(document: Document, pages: MutableList) { + return pages } override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt index e26317a93..468e97ef3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt @@ -1,18 +1,19 @@ package eu.kanade.tachiyomi.data.source.online.russian -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.text.SimpleDateFormat import java.util.* -class Mangachan(override val id: Int) : ParsedOnlineSource() { +class Mangachan : ParsedOnlineSource() { + + override val id: Long = 7 override val name = "Mangachan" @@ -22,23 +23,28 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites" + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) + } - override fun latestUpdatesInitialUrl() = "$baseUrl/newestch" - - override fun searchMangaInitialUrl(query: String, filters: List>): String { - if (query.isNotEmpty()) { - return "$baseUrl/?do=search&subaction=search&story=$query" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if (query.isNotEmpty()) { + "$baseUrl/?do=search&subaction=search&story=$query" } else { - val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE } + val filt = filters.filterIsInstance().filter { !it.isIgnored() } if (filt.isNotEmpty()) { var genres = "" - filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' } - return "$baseUrl/tags/${genres.dropLast(1)}" + filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' } + "$baseUrl/tags/${genres.dropLast(1)}" } else { - return "$baseUrl/?do=search&subaction=search&story=$query" + "$baseUrl/?do=search&subaction=search&story=$query" } } + return GET(url, headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/newestch?page=$page") } override fun popularMangaSelector() = "div.content_row" @@ -47,22 +53,26 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() { override fun searchMangaSelector() = popularMangaSelector() - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("h2 > a").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() element.select("a:nth-child(1)").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.text() } + return manga } - override fun searchMangaFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "a:contains(Вперед)" @@ -73,74 +83,80 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() { private fun searchGenresNextPageSelector() = popularMangaNextPageSelector() - override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List>) { + override fun searchMangaParse(response: Response): MangasPage { val document = response.asJsoup() - for (element in document.select(searchMangaSelector())) { - Manga.create(id).apply { - searchMangaFromElement(element, this) - page.mangas.add(this) - } - } - val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE } - searchMangaNextPageSelector().let { selector -> - if (page.nextPageUrl.isNullOrEmpty() && allIgnore) { - val onClick = document.select(selector).first()?.attr("onclick") - val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)")) - page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum - } + val mangas = document.select(searchMangaSelector()).map { element -> + searchMangaFromElement(element) } - searchGenresNextPageSelector().let { selector -> - if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) { - val url = document.select(selector).first()?.attr("href") - page.nextPageUrl = searchMangaInitialUrl(query, filters) + url - } - } + // FIXME +// val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE } +// searchMangaNextPageSelector().let { selector -> +// if (page.nextPageUrl.isNullOrEmpty() && allIgnore) { +// val onClick = document.select(selector).first()?.attr("onclick") +// val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)")) +// page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum +// } +// } +// +// searchGenresNextPageSelector().let { selector -> +// if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) { +// val url = document.select(selector).first()?.attr("href") +// page.nextPageUrl = searchMangaInitialUrl(query, filters) + url +// } +// } + + return MangasPage(mangas, false) } - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val infoElement = document.select("table.mangatitle").first() val descElement = document.select("div#description").first() val imgElement = document.select("img#cover").first() + val manga = SManga.create() manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text() manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.description = descElement.textNodes().first().text() manga.thumbnail_url = baseUrl + imgElement.attr("src") + return manga } private fun parseStatus(element: String): Int { when { - element.contains("перевод завершен") -> return Manga.COMPLETED - element.contains("перевод продолжается") -> return Manga.ONGOING - else -> return Manga.UNKNOWN + element.contains("перевод завершен") -> return SManga.COMPLETED + element.contains("перевод продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN } } override fun chapterListSelector() = "table.table_cha tr:gt(1)" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.name = urlElement.text() chapter.date_upload = element.select("div.date").first()?.text()?.let { SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time } ?: 0 + return chapter } - override fun pageListParse(response: Response, pages: MutableList) { + override fun pageListParse(response: Response): List { val html = response.body().string() val beginIndex = html.indexOf("fullimg\":[") + 10 val endIndex = html.indexOf(",]", beginIndex) val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "") val pageUrls = trimmedHtml.split(',') - pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) } + return pageUrls.mapIndexed { i, url -> Page(i, "", url) } } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + throw Exception("Not used") } override fun imageUrlParse(document: Document) = "" @@ -152,7 +168,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() { * return `Genre("${id.replace("_", " ")}")` }).join(',\n') * on http://mangachan.me/ */ - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( Genre("18 плюс"), Genre("bdsm"), Genre("арт"), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt index cab2def3f..929747c2c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.data.source.online.russian -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource +import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -11,7 +11,9 @@ import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern -class Mintmanga(override val id: Int) : ParsedOnlineSource() { +class Mintmanga : ParsedOnlineSource() { + + override val id: Long = 6 override val name = "Mintmanga" @@ -21,77 +23,89 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate" + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) + } - override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated" - - override fun searchMangaInitialUrl(query: String, filters: List>) = - "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}" + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) + } override fun popularMangaSelector() = "div.desc" override fun latestUpdatesSelector() = "div.desc" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("h3 > a").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.attr("title") } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") + return GET("$baseUrl/search?q=$query&$genres", headers) + } + override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) } // max 200 results override fun searchMangaNextPageSelector() = null - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val infoElement = document.select("div.leftContent").first() + val manga = SManga.create() manga.author = infoElement.select("span.elem_author").first()?.text() manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") manga.description = infoElement.select("div.manga-description").text() manga.status = parseStatus(infoElement.html()) manga.thumbnail_url = infoElement.select("img").attr("data-full") + return manga } private fun parseStatus(element: String): Int { when { - element.contains("

Запрещена публикация произведения по копирайту

") -> return Manga.LICENSED - element.contains("

Сингл") || element.contains("Перевод: завершен") -> return Manga.COMPLETED - element.contains("Перевод: продолжается") -> return Manga.ONGOING - else -> return Manga.UNKNOWN + element.contains("

Запрещена публикация произведения по копирайту

") -> return SManga.LICENSED + element.contains("

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED + element.contains("Перевод: продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN } } override fun chapterListSelector() = "div.chapters-link tbody tr" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.name = urlElement.text().replace(" новое", "") chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time } ?: 0 + return chapter } - override fun prepareNewChapter(chapter: Chapter, manga: Manga) { + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { chapter.chapter_number = -2f } - override fun pageListParse(response: Response, pages: MutableList) { + override fun pageListParse(response: Response): List { val html = response.body().string() val beginIndex = html.indexOf("rm_h.init( [") val endIndex = html.indexOf("], 0, false);", beginIndex) @@ -100,14 +114,18 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() { val p = Pattern.compile("'.+?','.+?',\".+?\"") val m = p.matcher(trimmedHtml) + val pages = mutableListOf() + var i = 0 while (m.find()) { val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) } + return pages } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + throw Exception("Not used") } override fun imageUrlParse(document: Document) = "" @@ -119,7 +137,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() { * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * on http://mintmanga.com/search */ - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( Genre("арт", "el_2220"), Genre("бара", "el_1353"), Genre("боевик", "el_1346"), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt index 0b75bc0da..de74721b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.data.source.online.russian -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.network.GET +import eu.kanade.tachiyomi.data.source.model.* import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource +import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -11,7 +11,9 @@ import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern -class Readmanga(override val id: Int) : ParsedOnlineSource() { +class Readmanga : ParsedOnlineSource() { + + override val id: Long = 5 override val name = "Readmanga" @@ -21,77 +23,89 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() { override val supportsLatest = true - override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate" - - override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated" - - override fun searchMangaInitialUrl(query: String, filters: List>) = - "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}" - override fun popularMangaSelector() = "div.desc" override fun latestUpdatesSelector() = "div.desc" - override fun popularMangaFromElement(element: Element, manga: Manga) { + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() element.select("h3 > a").first().let { manga.setUrlWithoutDomain(it.attr("href")) manga.title = it.attr("title") } + return manga } - override fun latestUpdatesFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) } override fun popularMangaNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink" + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") + return GET("$baseUrl/search?q=$query&$genres", headers) + } + override fun searchMangaSelector() = popularMangaSelector() - override fun searchMangaFromElement(element: Element, manga: Manga) { - popularMangaFromElement(element, manga) + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) } // max 200 results override fun searchMangaNextPageSelector() = null - override fun mangaDetailsParse(document: Document, manga: Manga) { + override fun mangaDetailsParse(document: Document): SManga { val infoElement = document.select("div.leftContent").first() + val manga = SManga.create() manga.author = infoElement.select("span.elem_author").first()?.text() manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") manga.description = infoElement.select("div.manga-description").text() manga.status = parseStatus(infoElement.html()) manga.thumbnail_url = infoElement.select("img").attr("data-full") + return manga } private fun parseStatus(element: String): Int { when { - element.contains("

Запрещена публикация произведения по копирайту

") -> return Manga.LICENSED - element.contains("

Сингл") || element.contains("Перевод: завершен") -> return Manga.COMPLETED - element.contains("Перевод: продолжается") -> return Manga.ONGOING - else -> return Manga.UNKNOWN + element.contains("

Запрещена публикация произведения по копирайту

") -> return SManga.LICENSED + element.contains("

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED + element.contains("Перевод: продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN } } override fun chapterListSelector() = "div.chapters-link tbody tr" - override fun chapterFromElement(element: Element, chapter: Chapter) { + override fun chapterFromElement(element: Element): SChapter { val urlElement = element.select("a").first() + val chapter = SChapter.create() chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.name = urlElement.text().replace(" новое", "") chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time } ?: 0 + return chapter } - override fun prepareNewChapter(chapter: Chapter, manga: Manga) { + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { chapter.chapter_number = -2f } - override fun pageListParse(response: Response, pages: MutableList) { + override fun pageListParse(response: Response): List { val html = response.body().string() val beginIndex = html.indexOf("rm_h.init( [") val endIndex = html.indexOf("], 0, false);", beginIndex) @@ -100,14 +114,18 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() { val p = Pattern.compile("'.+?','.+?',\".+?\"") val m = p.matcher(trimmedHtml) + val pages = mutableListOf() + var i = 0 while (m.find()) { val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) } + return pages } - override fun pageListParse(document: Document, pages: MutableList) { + override fun pageListParse(document: Document): List { + throw Exception("Not used") } override fun imageUrlParse(document: Document) = "" @@ -119,7 +137,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() { * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * on http://readmanga.me/search */ - override fun getFilterList(): List> = listOf( + override fun getFilterList() = FilterList( Genre("арт", "el_5685"), Genre("боевик", "el_2155"), Genre("боевые искусства", "el_2143"), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index 388d93f09..36ff18840 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -14,6 +14,7 @@ import com.afollestad.materialdialogs.MaterialDialog import com.f2prateek.rx.preferences.Preference import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.source.model.FilterList import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment @@ -32,7 +33,6 @@ import nucleus.factory.RequiresPresenter import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.subjects.PublishSubject -import timber.log.Timber import java.util.concurrent.TimeUnit.MILLISECONDS /** @@ -104,6 +104,11 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie private val toolbar: Toolbar get() = (activity as MainActivity).toolbar + /** + * Snackbar containing an error message when a request fails. + */ + private var snack: Snackbar? = null + /** * Navigation view containing filter items. */ @@ -201,8 +206,7 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie } else if (source != presenter.source) { selectedIndex = position showProgressBar() - glm.scrollToPositionWithOffset(0, 0) - llm.scrollToPositionWithOffset(0, 0) + adapter.clear() presenter.setActiveSource(source) navView?.setFilters(presenter.sourceFilters) activity.invalidateOptionsMenu() @@ -233,14 +237,14 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie } navView.onSearchClicked = { - val allDefault = (0..navView.adapter.items.lastIndex) - .none { navView.adapter.items[it].state != presenter.source.filters[it].state } - - presenter.setSourceFilter(if (allDefault) emptyList() else navView.adapter.items) + val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList()) + showProgressBar() + adapter.clear() + presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items) } navView.onResetClicked = { - presenter.appliedFilters = emptyList() + presenter.appliedFilters = FilterList() val newFilters = presenter.source.getFilterList() presenter.sourceFilters = newFilters navView.setFilters(newFilters) @@ -277,7 +281,7 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie // Setup filters button menu.findItem(R.id.action_set_filter).apply { icon.mutate() - if (presenter.source.filters.isEmpty()) { + if (presenter.sourceFilters.isEmpty()) { isEnabled = false icon.alpha = 128 } else { @@ -355,8 +359,7 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie return showProgressBar() - catalogue_grid.layoutManager.scrollToPosition(0) - catalogue_list.layoutManager.scrollToPosition(0) + adapter.clear() presenter.restartPager(newQuery) } @@ -394,9 +397,11 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie */ fun onAddPageError(error: Throwable) { hideProgressBar() - Timber.e(error) - catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) { + val message = if (error is NoResultsException) "No results found" else (error.message ?: "") + + snack?.dismiss() + snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) { setAction(R.string.action_retry) { showProgressBar() presenter.requestNext() @@ -456,6 +461,8 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie */ private fun showProgressBar() { progress.visibility = ProgressBar.VISIBLE + snack?.dismiss() + snack = null } /** @@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie */ private fun showGridProgressBar() { progress_grid.visibility = ProgressBar.VISIBLE + snack?.dismiss() + snack = null } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt index 34cce832a..8c63d7bb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt @@ -9,7 +9,8 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.TextView import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter +import eu.kanade.tachiyomi.data.source.model.Filter +import eu.kanade.tachiyomi.data.source.model.FilterList import eu.kanade.tachiyomi.util.dpToPx import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.inflate @@ -38,14 +39,14 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: reset_btn.setOnClickListener { onResetClicked() } } - fun setFilters(items: List>) { + fun setFilters(items: FilterList) { adapter.items = items adapter.notifyDataSetChanged() } inner class Adapter : RecyclerView.Adapter() { - var items: List> = emptyList() + var items: FilterList = FilterList() override fun getItemCount(): Int { return items.size diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt index 3653f91c6..355eccf83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt @@ -1,28 +1,32 @@ package eu.kanade.tachiyomi.ui.catalogue +import eu.kanade.tachiyomi.data.source.CatalogueSource +import eu.kanade.tachiyomi.data.source.model.FilterList import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.online.OnlineSource -import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers -open class CataloguePager(val source: OnlineSource, val query: String, val filters: List>) : Pager() { +open class CataloguePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() { - override fun requestNext(transformer: (Observable) -> Observable): Observable { - val lastPage = lastPage - - val page = if (lastPage == null) - MangasPage(1) - else - MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } + override fun requestNext(): Observable { + val page = currentPage val observable = if (query.isBlank() && filters.isEmpty()) source.fetchPopularManga(page) else source.fetchSearchManga(page, query, filters) - return transformer(observable) - .doOnNext { results.onNext(it) } - .doOnNext { this@CataloguePager.lastPage = it } + return observable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { + if (it.mangas.isNotEmpty()) { + onPageReceived(it) + } else { + throw NoResultsException() + } + } } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index b6cb0bfa6..296728e5c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -6,12 +6,12 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.source.CatalogueSource import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.SourceManager -import eu.kanade.tachiyomi.data.source.model.MangasPage +import eu.kanade.tachiyomi.data.source.model.FilterList +import eu.kanade.tachiyomi.data.source.model.SManga import eu.kanade.tachiyomi.data.source.online.LoginSource -import eu.kanade.tachiyomi.data.source.online.OnlineSource -import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import rx.Observable import rx.Subscription @@ -55,7 +55,7 @@ open class CataloguePresenter : BasePresenter() { /** * Active source. */ - lateinit var source: OnlineSource + lateinit var source: CatalogueSource private set /** @@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter() { /** * Modifiable list of filters. */ - var sourceFilters: List> = emptyList() + var sourceFilters = FilterList() /** * List of filters used by the [Pager]. If empty alongside [query], the popular query is used. */ - var appliedFilters: List> = emptyList() + var appliedFilters = FilterList() /** * Pager containing a list of manga results. @@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter() { * @param query the query. * @param filters the current state of the filters (for search mode). */ - fun restartPager(query: String = this.query, filters: List> = this.appliedFilters) { + fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) { this.query = query this.appliedFilters = filters @@ -145,11 +145,17 @@ open class CataloguePresenter : BasePresenter() { // Create a new pager. pager = createPager(query, filters) + val sourceId = source.id + // Prepare the pager. pagerSubscription?.let { remove(it) } pagerSubscription = pager.results() - .subscribeReplay({ view, page -> - view.onAddPage(page.page, page.mangas) + .observeOn(Schedulers.io()) + .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } } + .doOnNext { initializeMangas(it.second) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeReplay({ view, pair -> + view.onAddPage(pair.first, pair.second) }, { view, error -> Timber.e(error) }) @@ -165,7 +171,7 @@ open class CataloguePresenter : BasePresenter() { if (!hasNextPage()) return pageSubscription?.let { remove(it) } - pageSubscription = pager.requestNext { getPageTransformer(it) } + pageSubscription = Observable.defer { pager.requestNext() } .subscribeFirst({ view, page -> // Nothing to do when onNext is emitted. }, CatalogueFragment::onAddPageError) @@ -175,7 +181,7 @@ open class CataloguePresenter : BasePresenter() { * Returns true if the last fetched page has a next page. */ fun hasNextPage(): Boolean { - return pager.hasNextPage() + return pager.hasNextPage } /** @@ -183,12 +189,12 @@ open class CataloguePresenter : BasePresenter() { * * @param source the new active source. */ - fun setActiveSource(source: OnlineSource) { + fun setActiveSource(source: CatalogueSource) { prefs.lastUsedCatalogueSource().set(source.id) this.source = source sourceFilters = source.getFilterList() - restartPager(query = "", filters = emptyList()) + restartPager(query = "", filters = FilterList()) } /** @@ -208,7 +214,7 @@ open class CataloguePresenter : BasePresenter() { initializerSubscription?.let { remove(it) } initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io()) .flatMap { Observable.from(it) } - .filter { !it.initialized } + .filter { it.thumbnail_url == null && !it.initialized } .concatMap { getMangaDetailsObservable(it) } .onBackpressureBuffer() .observeOn(AndroidSchedulers.mainThread()) @@ -221,41 +227,21 @@ open class CataloguePresenter : BasePresenter() { .apply { add(this) } } - /** - * Returns the function to apply to the observable of the list of manga from the source. - * - * @param observable the observable from the source. - * @return the function to apply. - */ - fun getPageTransformer(observable: Observable): Observable { - return observable.subscribeOn(Schedulers.io()) - .doOnNext { it.mangas.replace { networkToLocalManga(it) } } - .doOnNext { initializeMangas(it.mangas) } - .observeOn(AndroidSchedulers.mainThread()) - } - - /** - * Replaces an object in the list with another. - */ - fun MutableList.replace(block: (T) -> T) { - forEachIndexed { i, obj -> - set(i, block(obj)) - } - } - /** * Returns a manga from the database for the given manga from network. It creates a new entry * if the manga is not yet in the database. * - * @param networkManga the manga from network. + * @param sManga the manga from the source. * @return a manga from the database. */ - private fun networkToLocalManga(networkManga: Manga): Manga { - var localManga = db.getManga(networkManga.url, source.id).executeAsBlocking() + private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking() if (localManga == null) { - val result = db.insertManga(networkManga).executeAsBlocking() - networkManga.id = result.insertedId() - localManga = networkManga + val newManga = Manga.create(sManga.url, sManga.title, sourceId) + newManga.copyFrom(sManga) + val result = db.insertManga(newManga).executeAsBlocking() + newManga.id = result.insertedId() + localManga = newManga } return localManga } @@ -279,6 +265,7 @@ open class CataloguePresenter : BasePresenter() { return source.fetchMangaDetails(manga) .flatMap { networkManga -> manga.copyFrom(networkManga) + manga.initialized = true db.insertManga(manga).executeAsBlocking() Observable.just(manga) } @@ -290,13 +277,13 @@ open class CataloguePresenter : BasePresenter() { * * @return a source. */ - fun getLastUsedSource(): OnlineSource { + fun getLastUsedSource(): CatalogueSource { val id = prefs.lastUsedCatalogueSource().get() ?: -1 val source = sourceManager.get(id) if (!isValidSource(source)) { return findFirstValidSource() } - return source as OnlineSource + return source as CatalogueSource } /** @@ -320,14 +307,14 @@ open class CataloguePresenter : BasePresenter() { * * @return the index of the first valid source. */ - fun findFirstValidSource(): OnlineSource { + fun findFirstValidSource(): CatalogueSource { return sources.first { isValidSource(it) } } /** * Returns a list of enabled sources ordered by language and name. */ - open protected fun getEnabledSources(): List { + open protected fun getEnabledSources(): List { val languages = prefs.enabledLanguages().getOrDefault() val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() @@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter() { languages.add("en") } - return sourceManager.getOnlineSources() + return sourceManager.getCatalogueSources() .filter { it.lang in languages } .filterNot { it.id.toString() in hiddenCatalogues } .sortedBy { "(${it.lang}) ${it.name}" } @@ -365,13 +352,13 @@ open class CataloguePresenter : BasePresenter() { /** * Set the filter states for the current source. * - * @param filterStates a list of active filters. + * @param filters a list of active filters. */ - fun setSourceFilter(filters: List>) { + fun setSourceFilter(filters: FilterList) { restartPager(filters = filters) } - open fun createPager(query: String, filters: List>): Pager { + open fun createPager(query: String, filters: FilterList): Pager { return CataloguePager(source, query, filters) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt new file mode 100644 index 000000000..3ac0dbac8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt @@ -0,0 +1,3 @@ +package eu.kanade.tachiyomi.ui.catalogue + +class NoResultsException : Exception() \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt index 26cb466f6..7d228e4cb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt @@ -1,25 +1,31 @@ package eu.kanade.tachiyomi.ui.catalogue +import com.jakewharton.rxrelay.PublishRelay import eu.kanade.tachiyomi.data.source.model.MangasPage -import rx.subjects.PublishSubject +import eu.kanade.tachiyomi.data.source.model.SManga import rx.Observable /** * A general pager for source requests (latest updates, popular, search) */ -abstract class Pager { +abstract class Pager(var currentPage: Int = 1) { - protected var lastPage: MangasPage? = null + var hasNextPage = true + private set - protected val results = PublishSubject.create() + protected val results: PublishRelay>> = PublishRelay.create() - fun results(): Observable { + fun results(): Observable>> { return results.asObservable() } - fun hasNextPage(): Boolean { - return lastPage == null || lastPage?.nextPageUrl != null + abstract fun requestNext(): Observable + + fun onPageReceived(mangasPage: MangasPage) { + val page = currentPage + currentPage++ + hasNextPage = mangasPage.hasNextPage && !mangasPage.mangas.isEmpty() + results.call(Pair(page, mangasPage.mangas)) } - abstract fun requestNext(transformer: (Observable) -> Observable): Observable } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt index 6391f9d7c..9af870510 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt @@ -1,28 +1,22 @@ package eu.kanade.tachiyomi.ui.latest_updates +import eu.kanade.tachiyomi.data.source.CatalogueSource import eu.kanade.tachiyomi.data.source.model.MangasPage -import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.ui.catalogue.Pager import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers /** * LatestUpdatesPager inherited from the general Pager. */ -class LatestUpdatesPager(val source: OnlineSource): Pager() { +class LatestUpdatesPager(val source: CatalogueSource): Pager() { - override fun requestNext(transformer: (Observable) -> Observable): Observable { - val lastPage = lastPage - - val page = if (lastPage == null) - MangasPage(1) - else - MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } - - val observable = source.fetchLatestUpdates(page) - - return transformer(observable) - .doOnNext { results.onNext(it) } - .doOnNext { this@LatestUpdatesPager.lastPage = it } + override fun requestNext(): Observable { + return source.fetchLatestUpdates(currentPage) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { onPageReceived(it) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt index 1d224bdd1..03575aac2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt @@ -1,26 +1,26 @@ package eu.kanade.tachiyomi.ui.latest_updates +import eu.kanade.tachiyomi.data.source.CatalogueSource import eu.kanade.tachiyomi.data.source.Source -import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.model.FilterList import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.Pager -import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter /** * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter. */ class LatestUpdatesPresenter : CataloguePresenter() { - override fun createPager(query: String, filters: List>): Pager { + override fun createPager(query: String, filters: FilterList): Pager { return LatestUpdatesPager(source) } - override fun getEnabledSources(): List { + override fun getEnabledSources(): List { return super.getEnabledSources().filter { it.supportsLatest } } override fun isValidSource(source: Source?): Boolean { - return super.isValidSource(source) && (source as OnlineSource).supportsLatest + return super.isValidSource(source) && (source as CatalogueSource).supportsLatest } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index b872f3239..8b78c95b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -125,7 +125,7 @@ class LibraryPresenter : BasePresenter() { */ private fun applyFilters(map: Map>): Map> { // Cached list of downloaded manga directories given a source id. - val mangaDirectories = mutableMapOf>() + val mangaDirectories = mutableMapOf>() // Cached list of downloaded chapter directories for a manga. val chapterDirectories = mutableMapOf() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index 3d8be2801..16fdc9c05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -197,7 +197,7 @@ class ChaptersPresenter : BasePresenter() { /** * Returns an observable that updates the chapter list with the latest from the source. */ - fun getRemoteChaptersObservable() = source.fetchChapterList(manga) + fun getRemoteChaptersObservable() = Observable.defer { source.fetchChapterList(manga) } .subscribeOn(Schedulers.io()) .map { syncChaptersWithSource(db, it, manga, source) } .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt index 9cf122198..fe206a072 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt @@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.Source +import eu.kanade.tachiyomi.data.source.model.SManga import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.manga.MangaActivity @@ -122,9 +123,9 @@ class MangaInfoFragment : BaseRxFragment() { // Update status TextView. manga_status.setText(when (manga.status) { - Manga.ONGOING -> R.string.ongoing - Manga.COMPLETED -> R.string.completed - Manga.LICENSED -> R.string.licensed + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed else -> R.string.unknown }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index 35941b3d0..91f09f45c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -99,9 +99,10 @@ class MangaInfoPresenter : BasePresenter() { * @return manga information. */ private fun fetchMangaObs(): Observable { - return source.fetchMangaDetails(manga) + return Observable.defer { source.fetchMangaDetails(manga) } .flatMap { networkManga -> manga.copyFrom(networkManga) + manga.initialized = true db.insertManga(manga).executeAsBlocking() Observable.just(manga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt index 0da76894a..9516d9690 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt @@ -4,6 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.fetchImageFromCacheThenNet +import eu.kanade.tachiyomi.data.source.online.fetchPageListFromCacheThenNet import eu.kanade.tachiyomi.util.plusAssign import rx.Observable import rx.schedulers.Schedulers @@ -36,9 +39,11 @@ class ChapterLoader( } private fun prepareOnlineReading() { + if (source !is OnlineSource) return + subscriptions += Observable.defer { Observable.just(queue.take().page) } .filter { it.status == Page.QUEUE } - .concatMap { source.fetchImage(it) } + .concatMap { source.fetchImageFromCacheThenNet(it) } .repeat() .subscribeOn(Schedulers.io()) .subscribe({ @@ -57,6 +62,10 @@ class ChapterLoader( Observable.just(chapter.pages!!) } .doOnNext { pages -> + if (pages.isEmpty()) { + throw Exception("Page list is empty") + } + // Now that the number of pages is known, fix the requested page if the last one // was requested. if (chapter.requestedPage == -1) { @@ -76,8 +85,8 @@ class ChapterLoader( // Fetch the page list from disk. downloadManager.buildPageList(source, manga, chapter) } else { - // Fetch the page list from cache or fallback to network - source.fetchPageList(chapter) + (source as? OnlineSource)?.fetchPageListFromCacheThenNet(chapter) + ?: source.fetchPageList(chapter) } } .doOnNext { pages -> @@ -111,6 +120,8 @@ class ChapterLoader( queue.offer(PriorityPage(page, 2)) } + + private data class PriorityPage(val page: Page, val priority: Int): Comparable { companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index d04a9c56f..d1b64e1ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -372,7 +372,9 @@ class ReaderPresenter : BasePresenter() { Observable.fromCallable { // Cache current page list progress for online chapters to allow a faster reopen if (!chapter.isDownloaded) { - source.let { if (it is OnlineSource) it.savePageList(chapter, pages) } + source.let { + if (it is OnlineSource) chapterCache.putPageListToCache(chapter, pages) + } } try { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt index 4c9c99057..69ffdaeb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt @@ -130,7 +130,7 @@ class RecentChaptersPresenter : BasePresenter() { */ private fun setDownloadedChapters(chapters: List) { // Cached list of downloaded manga directories. - val mangaDirectories = mutableMapOf>() + val mangaDirectories = mutableMapOf>() // Cached list of downloaded chapter directories for a manga. val chapterDirectories = mutableMapOf>() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt index 4e25168eb..5360469e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt @@ -123,13 +123,14 @@ class SettingsSourcesFragment : SettingsFragment() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SOURCE_CHANGE_REQUEST) { - val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference + if (requestCode == SOURCE_CHANGE_REQUEST && data != null) { + val sourceId = data.getLongExtra("key", -1L) + val pref = findPreference(getSourceKey(sourceId)) as? LoginCheckBoxPreference pref?.notifyChanged() } } - private fun getSourceKey(sourceId: Int): String { + private fun getSourceKey(sourceId: Long): String { return "source_$sourceId" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt index d6948d7c6..a5617fbcd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingFragment.kt @@ -81,8 +81,9 @@ class SettingsTrackingFragment : SettingsFragment() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SYNC_CHANGE_REQUEST) { - updatePreference(resultCode) + if (requestCode == SYNC_CHANGE_REQUEST && data != null) { + val serviceId = data.getIntExtra("key", -1) + updatePreference(serviceId) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt index 1f7e96776..5b503e2d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.Source +import eu.kanade.tachiyomi.data.source.model.SChapter import eu.kanade.tachiyomi.data.source.online.OnlineSource import java.util.* @@ -11,23 +12,29 @@ import java.util.* * Helper method for syncing the list of chapters from the source with the ones from the database. * * @param db the database. - * @param sourceChapters a list of chapters from the source. + * @param rawSourceChapters a list of chapters from the source. * @param manga the manga of the chapters. * @param source the source of the chapters. * @return a pair of new insertions and deletions. */ fun syncChaptersWithSource(db: DatabaseHelper, - sourceChapters: List, + rawSourceChapters: List, manga: Manga, source: Source) : Pair, List> { + if (rawSourceChapters.isEmpty()) { + throw Exception("No chapters found") + } + // Chapters from db. val dbChapters = db.getChapters(manga).executeAsBlocking() - // Fix manga id and order in source. - sourceChapters.forEachIndexed { i, chapter -> - chapter.manga_id = manga.id - chapter.source_order = i + val sourceChapters = rawSourceChapters.mapIndexed { i, sChapter -> + Chapter.create().apply { + copyFrom(sChapter) + manga_id = manga.id + source_order = i + } } // Chapters from the source not in db. diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java b/app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java deleted file mode 100644 index 5918013c6..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/UrlUtil.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.kanade.tachiyomi.util; - -import java.net.URI; -import java.net.URISyntaxException; - -public final class UrlUtil { - - private UrlUtil() throws InstantiationException { - throw new InstantiationException("This class is not for instantiation"); - } - - public static String getPath(String s) { - try { - URI uri = new URI(s); - String out = uri.getPath(); - if (uri.getQuery() != null) - out += "?" + uri.getQuery(); - if (uri.getFragment() != null) - out += "#" + uri.getFragment(); - return out; - } catch (URISyntaxException e) { - return s; - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt index 3fab969ea..150b1e4b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt @@ -21,10 +21,11 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2) * @param length the duration of the snack. * @param f a function to execute in the snack, allowing for example to define a custom action. */ -inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) { +inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit): Snackbar { val snack = Snackbar.make(this, message, length) val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView textView.setTextColor(Color.WHITE) snack.f() snack.show() + return snack } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index 95301c45d..a83d9712b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.widget.preference +import android.app.Activity import android.app.Dialog import android.content.DialogInterface import android.content.Intent @@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() { override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) - targetFragment?.onActivityResult(targetRequestCode, arguments.getInt("key"), Intent()) + val intent = Intent().putExtras(arguments) + targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, intent) } protected abstract fun checkLogin() diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt index 7e65bb879..48e6549e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt @@ -19,7 +19,7 @@ class SourceLoginDialog : LoginDialogPreference() { fun newInstance(source: Source): LoginDialogPreference { val fragment = SourceLoginDialog() val bundle = Bundle(1) - bundle.putInt("key", source.id) + bundle.putLong("key", source.id) fragment.arguments = bundle return fragment } @@ -32,7 +32,7 @@ class SourceLoginDialog : LoginDialogPreference() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val sourceId = arguments.getInt("key") + val sourceId = arguments.getLong("key") source = sourceManager.get(sourceId) as LoginSource } diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 1ce6054a8..4191408f6 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -64,7 +64,7 @@ automatic_updates pref_display_catalogue_as_list - pref_last_catalogue_source_key + last_catalogue_source download_new diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt index 0c0b67600..5179e39a8 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt @@ -9,12 +9,13 @@ import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.SourceManager +import eu.kanade.tachiyomi.data.source.model.SChapter import eu.kanade.tachiyomi.data.source.online.OnlineSource import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Matchers.anyInt +import org.mockito.Matchers.anyLong import org.mockito.Mockito import org.mockito.Mockito.* import org.robolectric.Robolectric @@ -51,7 +52,7 @@ class LibraryUpdateServiceTest { service = Robolectric.setupService(LibraryUpdateService::class.java) source = mock(OnlineSource::class.java) - `when`(service.sourceManager.get(anyInt())).thenReturn(source) + `when`(service.sourceManager.get(anyLong())).thenReturn(source) } @Test @@ -91,7 +92,7 @@ class LibraryUpdateServiceTest { // One of the updates will fail `when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters)) - `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error>(Exception())) + `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error>(Exception())) `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3)) val intent = Intent() @@ -117,8 +118,7 @@ class LibraryUpdateServiceTest { private fun createManga(vararg urls: String): List { val list = ArrayList() for (url in urls) { - val m = Manga.create(url) - m.title = url.substring(1) + val m = Manga.create(url, url.substring(1)) m.favorite = true list.add(m) }