mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Initial support for external sources
This commit is contained in:
		| @@ -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<List<Page>> { | ||||
|         return Observable.fromCallable<List<Page>> { | ||||
|     fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> { | ||||
|         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<Page>) { | ||||
|     fun putPageListToCache(chapter: Chapter, pages: List<Page>) { | ||||
|         // 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}" | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() { | ||||
|  | ||||
|     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)) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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<UniFile> { | ||||
|         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 { | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { | ||||
|     /** | ||||
|      * Map where request headers are stored for a source. | ||||
|      */ | ||||
|     private val cachedHeaders = hashMapOf<Int, LazyHeaders>() | ||||
|     private val cachedHeaders = hashMapOf<Long, LazyHeaders>() | ||||
|  | ||||
|     /** | ||||
|      * Factory class for creating [MangaModelLoader] instances. | ||||
|   | ||||
| @@ -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<Manga>() | ||||
|  | ||||
|                     source.fetchMangaDetails(manga) | ||||
|                             .doOnNext { networkManga -> | ||||
|                             .map { networkManga -> | ||||
|                                 manga.copyFrom(networkManga) | ||||
|                                 db.insertManga(manga).executeAsBlocking() | ||||
|                                 manga | ||||
|                             } | ||||
|                             .onErrorReturn { manga } | ||||
|                 } | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable containing a page with a list of manga. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      * @param query the search query. | ||||
|      * @param filters the list of filters to apply. | ||||
|      */ | ||||
|     fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable containing a page with a list of latest manga updates. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      */ | ||||
|     fun fetchLatestUpdates(page: Int): Observable<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns the list of filters for the source. | ||||
|      */ | ||||
|     fun getFilterList(): FilterList | ||||
| } | ||||
| @@ -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<Manga> | ||||
|     fun fetchMangaDetails(manga: SManga): Observable<SManga> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable with all the available chapters for a manga. | ||||
|      * | ||||
|      * @param manga the manga to update. | ||||
|      */ | ||||
|     fun fetchChapterList(manga: Manga): Observable<List<Chapter>> | ||||
|     fun fetchChapterList(manga: SManga): Observable<List<SChapter>> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable with the list of pages a chapter has. | ||||
|      * | ||||
|      * @param chapter the chapter. | ||||
|      */ | ||||
|     fun fetchPageList(chapter: Chapter): Observable<List<Page>> | ||||
|     fun fetchPageList(chapter: SChapter): Observable<List<Page>> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable with the path of the image. | ||||
|      * | ||||
|      * @param page the page. | ||||
|      */ | ||||
|     fun fetchImage(page: Page): Observable<Page> | ||||
| } | ||||
| @@ -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<Long, Source>() | ||||
|  | ||||
|     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<OnlineSource>() | ||||
|  | ||||
|     private fun createOnlineSourceList(): List<Source> = 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<CatalogueSource>() | ||||
|  | ||||
|     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<Source> = listOf( | ||||
|             Batoto(), | ||||
|             Mangahere(), | ||||
|             Mangafox(), | ||||
|             Kissmanga(), | ||||
|             Readmanga(), | ||||
|             Mintmanga(), | ||||
|             Mangachan(), | ||||
|             Readmangatoday(), | ||||
|             Mangasee(), | ||||
|             WieManga() | ||||
|     ) | ||||
|  | ||||
|     private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply { | ||||
|         createOnlineSourceList().forEach { put(it.id, it) } | ||||
|     private fun createYamlSources(): List<Source> { | ||||
|         val sources = mutableListOf<Source>() | ||||
|  | ||||
|         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<OnlineSource> { | ||||
|         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<OnlineSource>() | ||||
|         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 | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| sealed class Filter<T>(val name: String, var state: T) { | ||||
|     open class Header(name: String) : Filter<Any>(name, 0) | ||||
|     abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) | ||||
|     abstract class Text(name: String, state: String = "") : Filter<String>(name, state) | ||||
|     abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) | ||||
|     abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { | ||||
|         fun isIgnored() = state == STATE_IGNORE | ||||
|         fun isIncluded() = state == STATE_INCLUDE | ||||
|         fun isExcluded() = state == STATE_EXCLUDE | ||||
|         companion object { | ||||
|             const val STATE_IGNORE = 0 | ||||
|             const val STATE_INCLUDE = 1 | ||||
|             const val STATE_EXCLUDE = 2 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| class FilterList(list: List<Filter<*>>) : List<Filter<*>> 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 } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<Manga> = mutableListOf() | ||||
|  | ||||
|     lateinit var url: String | ||||
|  | ||||
|     var nextPageUrl: String? = null | ||||
|  | ||||
| } | ||||
| data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean) | ||||
| @@ -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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
| } | ||||
| @@ -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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
| } | ||||
| @@ -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<MangasPage> = 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<MangasPage> { | ||||
|         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<Filter<*>>): Observable<MangasPage> = 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<Filter<*>>): 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<MangasPage> { | ||||
|         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<Filter<*>>): 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<Filter<*>>) | ||||
|     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<MangasPage> = client | ||||
|             .newCall(latestUpdatesRequest(page)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 latestUpdatesParse(response, page) | ||||
|                 page | ||||
|             } | ||||
|     override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { | ||||
|         return client.newCall(latestUpdatesRequest(page)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     latestUpdatesParse(response) | ||||
|                 } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the request for latest manga given the page. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      */ | ||||
|     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<Manga> = client | ||||
|             .newCall(mangaDetailsRequest(manga)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 Manga.create(manga.url, id).apply { | ||||
|                     mangaDetailsParse(response, this) | ||||
|                     initialized = true | ||||
|     override fun fetchMangaDetails(manga: SManga): Observable<SManga> { | ||||
|         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<List<Chapter>> = client | ||||
|             .newCall(chapterListRequest(manga)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 mutableListOf<Chapter>().apply { | ||||
|                     chapterListParse(response, this) | ||||
|                     if (isEmpty()) { | ||||
|                         throw Exception("No chapters found") | ||||
|                     } | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         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<Chapter>) | ||||
|     abstract protected fun chapterListParse(response: Response): List<SChapter> | ||||
|  | ||||
|     /** | ||||
|      * 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<List<Page>> = 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<List<Page>> = client | ||||
|             .newCall(pageListRequest(chapter)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 mutableListOf<Page>().apply { | ||||
|                     pageListParse(response, this) | ||||
|                     if (isEmpty()) { | ||||
|                         throw Exception("Page list is empty") | ||||
|                     } | ||||
|     override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { | ||||
|         return client.newCall(pageListRequest(chapter)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     pageListParse(response) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the request for getting the page list. Override only if it's needed to override the | ||||
|      * url, send different headers or request method like POST. | ||||
|      * | ||||
|      * @param chapter the chapter whose page list has to be fetched | ||||
|      * @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<Page>) | ||||
|  | ||||
|     /** | ||||
|      * 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<Page> | ||||
|  | ||||
|     /** | ||||
|      * 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> { | ||||
|         page.status = Page.LOAD_PAGE | ||||
|         return client | ||||
|                 .newCall(imageUrlRequest(page)) | ||||
|     open fun fetchImageUrl(page: Page): Observable<String> { | ||||
|         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<Page> = | ||||
|             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<Response> = client | ||||
|             .newCallWithProgress(imageRequest(page), page) | ||||
|             .asObservableSuccess() | ||||
|     fun fetchImage(page: Page): Observable<Response> { | ||||
|         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<Page> { | ||||
|         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> { | ||||
|         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<Page>) = Observable.from(pages) | ||||
|             .filter { !it.imageUrl.isNullOrEmpty() } | ||||
|             .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) | ||||
|  | ||||
|     fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages) | ||||
|             .filter { it.imageUrl.isNullOrEmpty() } | ||||
|             .concatMap { fetchImageUrl(it) } | ||||
|  | ||||
|     fun savePageList(chapter: Chapter, pages: List<Page>?) { | ||||
|         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<T>(val name: String, var state: T) { | ||||
|         open class Header(name: String) : Filter<Any>(name, 0) | ||||
|         abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) | ||||
|         abstract class Text(name: String, state: String = "") : Filter<String>(name, state) | ||||
|         abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) | ||||
|         abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { | ||||
|             companion object { | ||||
|                 const val STATE_IGNORE = 0 | ||||
|                 const val STATE_INCLUDE = 1 | ||||
|                 const val STATE_EXCLUDE = 2 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     open fun getFilterList(): List<Filter<*>> = emptyList() | ||||
|     /** | ||||
|      * Returns the list of filters for the source. | ||||
|      */ | ||||
|     override fun getFilterList() = FilterList() | ||||
| } | ||||
|   | ||||
| @@ -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<List<Page>> { | ||||
|     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<Page> { | ||||
|     return if (page.imageUrl.isNullOrEmpty()) | ||||
|         getImageUrl(page).flatMap { getCachedImage(it) } | ||||
|     else | ||||
|         getCachedImage(page) | ||||
| } | ||||
|  | ||||
| fun OnlineSource.getImageUrl(page: Page): Observable<Page> { | ||||
|     page.status = Page.LOAD_PAGE | ||||
|     return fetchImageUrl(page) | ||||
|             .doOnError { page.status = Page.ERROR } | ||||
|             .onErrorReturn { null } | ||||
|             .doOnNext { page.imageUrl = it } | ||||
|             .map { page } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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<Page> { | ||||
|     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> { | ||||
|     page.status = Page.DOWNLOAD_IMAGE | ||||
|     return fetchImage(page) | ||||
|             .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) } | ||||
|             .map { page } | ||||
| } | ||||
|  | ||||
| fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { !it.imageUrl.isNullOrEmpty() } | ||||
|             .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) | ||||
| } | ||||
|  | ||||
| fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { it.imageUrl.isNullOrEmpty() } | ||||
|             .concatMap { getImageUrl(it) } | ||||
| } | ||||
| @@ -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 <a> 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<Filter<*>>) { | ||||
|     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 <a> 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 <a> tag, like [popularMangaNextPageSelector]. | ||||
|      * Returns the Jsoup selector that returns the <a> 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<Chapter>) { | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         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<Page>) { | ||||
|         pageListParse(response.asJsoup(), pages) | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         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<Page>) | ||||
|     abstract protected fun pageListParse(document: Document): List<Page> | ||||
|  | ||||
|     /** | ||||
|      * Parse the response from the site and returns the absolute url to the source image. | ||||
|   | ||||
| @@ -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<Filter<*>>): 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<Filter<*>>) = map.search.url.replace("\$query", query) | ||||
|  | ||||
|     override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) { | ||||
|     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<Chapter>) { | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val document = response.asJsoup() | ||||
|  | ||||
|         val chapters = mutableListOf<SChapter>() | ||||
|         with(map.chapters) { | ||||
|             val pool = emptyMap<String, Element>() | ||||
|             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<Page>) { | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val body = response.body().string() | ||||
|         val url = response.request().url().toString() | ||||
|  | ||||
|         val pages = mutableListOf<Page>() | ||||
|  | ||||
|         // 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 { | ||||
|   | ||||
| @@ -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<String, Any?>) : SelectableNode(map) { | ||||
|     fun getStatus(document: Element, cache: Map<String, Element>): 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 | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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<Filter<*>>) = searchMangaUrl(query, filters, 1) | ||||
|  | ||||
|     private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, 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<Filter<*>>): 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<Filter<*>>) { | ||||
|         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<Chapter>) { | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val pages = mutableListOf<Page>() | ||||
|         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<List<Chapter>> { | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         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<ListValue>, state: Int = 0) : Filter.List<ListValue>(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<Filter<*>> = 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(), | ||||
|   | ||||
| @@ -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<Filter<*>>): 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<Filter<*>>) = "$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<Page>) { | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val pages = mutableListOf<Page>() | ||||
|         //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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         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<Filter<*>> = 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") | ||||
|     ) | ||||
| } | ||||
| @@ -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<Filter<*>>): 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<Page>) { | ||||
|         val document = response.asJsoup() | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val url = document.baseUri().substringBeforeLast('/') | ||||
|  | ||||
|         val url = response.request().url().toString().substringBeforeLast('/') | ||||
|         val pages = mutableListOf<Page>() | ||||
|         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<Page>) { | ||||
|         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<Filter<*>> = 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"))), | ||||
|   | ||||
| @@ -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<Filter<*>>): 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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val pages = mutableListOf<Page>() | ||||
|         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<Filter<*>> = 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"))), | ||||
|   | ||||
| @@ -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<Filter<*>>): 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<Filter<*>>): 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<FormBody.Builder, String> { | ||||
|         val url = HttpUrl.parse(page.url) | ||||
|         val body = FormBody.Builder().add("page", page.page.toString()) | ||||
|     private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> { | ||||
|         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<Filter<*>>) { | ||||
|         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<Page>) { | ||||
|         val document = response.asJsoup() | ||||
|         val fullUrl = response.request().url().toString() | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val fullUrl = document.baseUri() | ||||
|         val url = fullUrl.substringBeforeLast('/') | ||||
|  | ||||
|         val pages = mutableListOf<Page>() | ||||
|  | ||||
|         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<Page>) { | ||||
|         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<Filter<*>> = 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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<Filter<*>>) = | ||||
|             "$baseUrl/service/advanced_search" | ||||
|  | ||||
|  | ||||
|     override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): 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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val pages = mutableListOf<Page>() | ||||
|         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<Filter<*>> = listOf( | ||||
|     override fun getFilterList() = FilterList( | ||||
|             TextField("Author", "author-name"), | ||||
|             TextField("Artist", "artist-name"), | ||||
|             Type(), | ||||
|   | ||||
| @@ -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<Filter<*>>) = "$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<Page>) { | ||||
|         val document = response.asJsoup() | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val pages = mutableListOf<Page>() | ||||
|  | ||||
|         document.select("select#page").first().select("option").forEach { | ||||
|             pages.add(Page(pages.size, it.attr("value"))) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun pageListParse(document: Document, pages: MutableList<Page>) { | ||||
|         return pages | ||||
|     } | ||||
|  | ||||
|     override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src") | ||||
|   | ||||
| @@ -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<Filter<*>>): 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<Genre>().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<Filter<*>>) { | ||||
|     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<Page>) { | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         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<Filter<*>> = listOf( | ||||
|     override fun getFilterList() = FilterList( | ||||
|             Genre("18 плюс"), | ||||
|             Genre("bdsm"), | ||||
|             Genre("арт"), | ||||
|   | ||||
| @@ -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<Filter<*>>) = | ||||
|             "$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<Genre>().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("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING | ||||
|             else -> return Manga.UNKNOWN | ||||
|             element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> 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<Page>) { | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         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<Page>() | ||||
|  | ||||
|         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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         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<Filter<*>> = listOf( | ||||
|     override fun getFilterList() = FilterList( | ||||
|             Genre("арт", "el_2220"), | ||||
|             Genre("бара", "el_1353"), | ||||
|             Genre("боевик", "el_1346"), | ||||
|   | ||||
| @@ -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<Filter<*>>) = | ||||
|             "$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<Genre>().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("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING | ||||
|             else -> return Manga.UNKNOWN | ||||
|             element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED | ||||
|             element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED | ||||
|             element.contains("<b>Перевод:</b> продолжается") -> 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<Page>) { | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         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<Page>() | ||||
|  | ||||
|         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<Page>) { | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         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<Filter<*>> = listOf( | ||||
|     override fun getFilterList() = FilterList( | ||||
|             Genre("арт", "el_5685"), | ||||
|             Genre("боевик", "el_2155"), | ||||
|             Genre("боевые искусства", "el_2143"), | ||||
|   | ||||
| @@ -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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), FlexibleVie | ||||
|      */ | ||||
|     private fun showProgressBar() { | ||||
|         progress.visibility = ProgressBar.VISIBLE | ||||
|         snack?.dismiss() | ||||
|         snack = null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie | ||||
|      */ | ||||
|     private fun showGridProgressBar() { | ||||
|         progress_grid.visibility = ProgressBar.VISIBLE | ||||
|         snack?.dismiss() | ||||
|         snack = null | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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<Filter<*>>) { | ||||
|     fun setFilters(items: FilterList) { | ||||
|         adapter.items = items | ||||
|         adapter.notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     inner class Adapter : RecyclerView.Adapter<Holder>() { | ||||
|  | ||||
|         var items: List<Filter<*>> = emptyList() | ||||
|         var items: FilterList = FilterList() | ||||
|  | ||||
|         override fun getItemCount(): Int { | ||||
|             return items.size | ||||
|   | ||||
| @@ -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<Filter<*>>) : Pager() { | ||||
| open class CataloguePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() { | ||||
|  | ||||
|     override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> { | ||||
|         val lastPage = lastPage | ||||
|  | ||||
|         val page = if (lastPage == null) | ||||
|             MangasPage(1) | ||||
|         else | ||||
|             MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } | ||||
|     override fun requestNext(): Observable<MangasPage> { | ||||
|         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() | ||||
|                     } | ||||
|                 } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<CatalogueFragment>() { | ||||
|     /** | ||||
|      * Active source. | ||||
|      */ | ||||
|     lateinit var source: OnlineSource | ||||
|     lateinit var source: CatalogueSource | ||||
|         private set | ||||
|  | ||||
|     /** | ||||
| @@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() { | ||||
|     /** | ||||
|      * Modifiable list of filters. | ||||
|      */ | ||||
|     var sourceFilters: List<Filter<*>> = emptyList() | ||||
|     var sourceFilters = FilterList() | ||||
|  | ||||
|     /** | ||||
|      * List of filters used by the [Pager]. If empty alongside [query], the popular query is used. | ||||
|      */ | ||||
|     var appliedFilters: List<Filter<*>> = emptyList() | ||||
|     var appliedFilters = FilterList() | ||||
|  | ||||
|     /** | ||||
|      * Pager containing a list of manga results. | ||||
| @@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() { | ||||
|      * @param query the query. | ||||
|      * @param filters the current state of the filters (for search mode). | ||||
|      */ | ||||
|     fun restartPager(query: String = this.query, filters: List<Filter<*>> = 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<CatalogueFragment>() { | ||||
|         // 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<CatalogueFragment>() { | ||||
|         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<CatalogueFragment>() { | ||||
|      * 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<CatalogueFragment>() { | ||||
|      * | ||||
|      * @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<CatalogueFragment>() { | ||||
|         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<CatalogueFragment>() { | ||||
|                 .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<MangasPage>): Observable<MangasPage> { | ||||
|         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 <T> MutableList<T>.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<CatalogueFragment>() { | ||||
|         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<CatalogueFragment>() { | ||||
|      * | ||||
|      * @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<CatalogueFragment>() { | ||||
|      * | ||||
|      * @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<OnlineSource> { | ||||
|     open protected fun getEnabledSources(): List<CatalogueSource> { | ||||
|         val languages = prefs.enabledLanguages().getOrDefault() | ||||
|         val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() | ||||
|  | ||||
| @@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() { | ||||
|             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<CatalogueFragment>() { | ||||
|     /** | ||||
|      * 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<Filter<*>>) { | ||||
|     fun setSourceFilter(filters: FilterList) { | ||||
|         restartPager(filters = filters) | ||||
|     } | ||||
|  | ||||
|     open fun createPager(query: String, filters: List<Filter<*>>): Pager { | ||||
|     open fun createPager(query: String, filters: FilterList): Pager { | ||||
|         return CataloguePager(source, query, filters) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,3 @@ | ||||
| package eu.kanade.tachiyomi.ui.catalogue | ||||
|  | ||||
| class NoResultsException : Exception() | ||||
| @@ -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<MangasPage>() | ||||
|     protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create() | ||||
|  | ||||
|     fun results(): Observable<MangasPage> { | ||||
|     fun results(): Observable<Pair<Int, List<SManga>>> { | ||||
|         return results.asObservable() | ||||
|     } | ||||
|  | ||||
|     fun hasNextPage(): Boolean { | ||||
|         return lastPage == null || lastPage?.nextPageUrl != null | ||||
|     abstract fun requestNext(): Observable<MangasPage> | ||||
|  | ||||
|     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<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> | ||||
| } | ||||
| @@ -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<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> { | ||||
|         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<MangasPage> { | ||||
|         return source.fetchLatestUpdates(currentPage) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .doOnNext { onPageReceived(it) } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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<Filter<*>>): Pager { | ||||
|     override fun createPager(query: String, filters: FilterList): Pager { | ||||
|         return LatestUpdatesPager(source) | ||||
|     } | ||||
|  | ||||
|     override fun getEnabledSources(): List<OnlineSource> { | ||||
|     override fun getEnabledSources(): List<CatalogueSource> { | ||||
|         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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -125,7 +125,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() { | ||||
|      */ | ||||
|     private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> { | ||||
|         // Cached list of downloaded manga directories given a source id. | ||||
|         val mangaDirectories = mutableMapOf<Int, Array<UniFile>>() | ||||
|         val mangaDirectories = mutableMapOf<Long, Array<UniFile>>() | ||||
|  | ||||
|         // Cached list of downloaded chapter directories for a manga. | ||||
|         val chapterDirectories = mutableMapOf<Long, Boolean>() | ||||
|   | ||||
| @@ -197,7 +197,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() { | ||||
|     /** | ||||
|      * 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()) | ||||
|   | ||||
| @@ -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<MangaInfoPresenter>() { | ||||
|  | ||||
|         // 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 | ||||
|         }) | ||||
|  | ||||
|   | ||||
| @@ -99,9 +99,10 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() { | ||||
|      * @return manga information. | ||||
|      */ | ||||
|     private fun fetchMangaObs(): Observable<Manga> { | ||||
|         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>(manga) | ||||
|                 } | ||||
|   | ||||
| @@ -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<PriorityPage> { | ||||
|  | ||||
|         companion object { | ||||
|   | ||||
| @@ -372,7 +372,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() { | ||||
|         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 { | ||||
|   | ||||
| @@ -130,7 +130,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() { | ||||
|      */ | ||||
|     private fun setDownloadedChapters(chapters: List<RecentChapter>) { | ||||
|         // Cached list of downloaded manga directories. | ||||
|         val mangaDirectories = mutableMapOf<Int, Array<UniFile>>() | ||||
|         val mangaDirectories = mutableMapOf<Long, Array<UniFile>>() | ||||
|  | ||||
|         // Cached list of downloaded chapter directories for a manga. | ||||
|         val chapterDirectories = mutableMapOf<Long, Array<UniFile>>() | ||||
|   | ||||
| @@ -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" | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<Chapter>, | ||||
|                            rawSourceChapters: List<SChapter>, | ||||
|                            manga: Manga, | ||||
|                            source: Source) : Pair<List<Chapter>, List<Chapter>> { | ||||
|  | ||||
|     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. | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,7 @@ | ||||
|     <string name="pref_enable_automatic_updates_key">automatic_updates</string> | ||||
|  | ||||
|     <string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string> | ||||
|     <string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string> | ||||
|     <string name="pref_last_catalogue_source_key">last_catalogue_source</string> | ||||
|  | ||||
|     <string name="pref_download_new_key">download_new</string> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user