Browse Source

Move source and network outside data

len 8 years ago
parent
commit
706163e7a6
95 changed files with 4082 additions and 4082 deletions
  1. 2 2
      app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
  3. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt
  4. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt
  5. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
  6. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
  7. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt
  8. 4 4
      app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
  9. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt
  10. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt
  11. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt
  12. 2 2
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt
  13. 3 3
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
  14. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
  15. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt
  16. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
  17. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt
  18. 4 4
      app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt
  19. 4 4
      app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt
  20. 79 79
      app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt
  21. 38 38
      app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
  22. 69 69
      app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt
  23. 18 18
      app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt
  24. 74 74
      app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt
  25. 4 4
      app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt
  26. 39 39
      app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt
  27. 32 32
      app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
  28. 45 45
      app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt
  29. 43 43
      app/src/main/java/eu/kanade/tachiyomi/source/Source.kt
  30. 159 159
      app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
  31. 1 1
      app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
  32. 6 6
      app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
  33. 2 2
      app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt
  34. 47 47
      app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt
  35. 1 1
      app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
  36. 1 1
      app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt
  37. 1 1
      app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
  38. 1 1
      app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt
  39. 14 14
      app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt
  40. 361 361
      app/src/main/java/eu/kanade/tachiyomi/source/online/OnlineSource.kt
  41. 98 98
      app/src/main/java/eu/kanade/tachiyomi/source/online/OnlineSourceFetcher.kt
  42. 200 200
      app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedOnlineSource.kt
  43. 232 232
      app/src/main/java/eu/kanade/tachiyomi/source/online/YamlOnlineSource.kt
  44. 233 233
      app/src/main/java/eu/kanade/tachiyomi/source/online/YamlOnlineSourceMappings.kt
  45. 365 365
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt
  46. 196 196
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt
  47. 222 222
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt
  48. 219 219
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt
  49. 242 242
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt
  50. 218 218
      app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt
  51. 121 121
      app/src/main/java/eu/kanade/tachiyomi/source/online/german/WieManga.kt
  52. 229 229
      app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt
  53. 183 183
      app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt
  54. 182 182
      app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt
  55. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
  56. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt
  57. 7 7
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  58. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt
  59. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt
  60. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt
  61. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt
  62. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt
  63. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt
  64. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt
  65. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt
  66. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortItem.kt
  67. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt
  68. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt
  69. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt
  70. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt
  71. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
  72. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  73. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  74. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
  75. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
  76. 5 5
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt
  77. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
  78. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt
  79. 3 3
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  80. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt
  81. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt
  82. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt
  83. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt
  84. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt
  85. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt
  86. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt
  87. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt
  88. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt
  89. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt
  90. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt
  91. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt
  92. 3 3
      app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt
  93. 2 2
      app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt
  94. 3 3
      app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt
  95. 3 3
      app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt

@@ -6,9 +6,9 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.data.track.TrackManager
 import uy.kohesive.injekt.api.InjektModule
 import uy.kohesive.injekt.api.InjektRegistrar

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt

@@ -6,7 +6,7 @@ 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.source.model.Page
 import eu.kanade.tachiyomi.util.DiskUtil
 import eu.kanade.tachiyomi.util.saveTo
 import okhttp3.Response

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt

@@ -1,6 +1,6 @@
 package eu.kanade.tachiyomi.data.database.models
 
-import eu.kanade.tachiyomi.data.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SChapter
 import java.io.Serializable
 
 interface Chapter : SChapter, Serializable {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt

@@ -1,6 +1,6 @@
 package eu.kanade.tachiyomi.data.database.models
 
-import eu.kanade.tachiyomi.data.source.model.SManga
+import eu.kanade.tachiyomi.source.model.SManga
 
 interface Manga : SManga {
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt

@@ -6,8 +6,8 @@ import com.jakewharton.rxrelay.BehaviorRelay
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.model.DownloadQueue
-import eu.kanade.tachiyomi.data.source.Source
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.model.Page
 import rx.Observable
 
 /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt

@@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 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.Source
+import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.util.DiskUtil
 import uy.kohesive.injekt.injectLazy
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt

@@ -5,8 +5,8 @@ import com.google.gson.Gson
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.source.SourceManager
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import uy.kohesive.injekt.injectLazy
 
 /**

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

@@ -10,10 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.download.model.DownloadQueue
 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.source.SourceManager
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
 import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
 import eu.kanade.tachiyomi.util.RetryWithDelay
 import eu.kanade.tachiyomi.util.plusAssign

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt

@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.download.model
 
 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.online.OnlineSource
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import rx.subjects.PublishSubject
 
 class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.download.model
 import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.download.DownloadStore
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import rx.Observable
 import rx.subjects.PublishSubject
 import java.util.concurrent.CopyOnWriteArrayList

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/glide/AppGlideModule.kt

@@ -8,7 +8,7 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
 import com.bumptech.glide.load.model.GlideUrl
 import com.bumptech.glide.module.GlideModule
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.io.InputStream

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt

@@ -8,8 +8,8 @@ import com.bumptech.glide.load.model.*
 import com.bumptech.glide.load.model.stream.StreamModelLoader
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.source.SourceManager
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import uy.kohesive.injekt.injectLazy
 import java.io.File
 import java.io.InputStream

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 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.source.SourceManager
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.*
 import rx.Observable

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -7,7 +7,7 @@ import android.preference.PreferenceManager
 import com.f2prateek.rx.preferences.Preference
 import com.f2prateek.rx.preferences.RxSharedPreferences
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.Source
+import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.data.track.TrackService
 import java.io.File
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track
 import android.support.annotation.CallSuper
 import android.support.annotation.DrawableRes
 import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import okhttp3.OkHttpClient
 import rx.Completable

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt

@@ -5,7 +5,7 @@ import com.github.salomonbrys.kotson.int
 import com.github.salomonbrys.kotson.string
 import com.google.gson.JsonObject
 import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.network.POST
+import eu.kanade.tachiyomi.network.POST
 import okhttp3.FormBody
 import okhttp3.OkHttpClient
 import okhttp3.ResponseBody

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.kitsu
 import com.github.salomonbrys.kotson.*
 import com.google.gson.JsonObject
 import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.network.POST
+import eu.kanade.tachiyomi.network.POST
 import okhttp3.FormBody
 import okhttp3.OkHttpClient
 import retrofit2.Retrofit

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyanimelistApi.kt

@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.myanimelist
 import android.net.Uri
 import android.util.Xml
 import eu.kanade.tachiyomi.data.database.models.Track
-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.network.asObservableSuccess
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservable
+import eu.kanade.tachiyomi.network.asObservableSuccess
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.util.selectInt
 import eu.kanade.tachiyomi.util.selectText

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt

@@ -8,10 +8,10 @@ import android.content.Intent
 import android.content.IntentFilter
 import android.os.Build
 import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.data.network.GET
-import eu.kanade.tachiyomi.data.network.NetworkHelper
-import eu.kanade.tachiyomi.data.network.ProgressListener
-import eu.kanade.tachiyomi.data.network.newCallWithProgress
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.ProgressListener
+import eu.kanade.tachiyomi.network.newCallWithProgress
 import eu.kanade.tachiyomi.util.registerLocalReceiver
 import eu.kanade.tachiyomi.util.saveTo
 import eu.kanade.tachiyomi.util.sendLocalBroadcastSync

+ 79 - 79
app/src/main/java/eu/kanade/tachiyomi/data/network/CloudflareInterceptor.kt → app/src/main/java/eu/kanade/tachiyomi/network/CloudflareInterceptor.kt

@@ -1,80 +1,80 @@
-package eu.kanade.tachiyomi.data.network
-
-import com.squareup.duktape.Duktape
-import okhttp3.HttpUrl
-import okhttp3.Interceptor
-import okhttp3.Request
-import okhttp3.Response
-
-class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
-
-    //language=RegExp
-    private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
-    
-    //language=RegExp
-    private val passPattern = Regex("""name="pass" value="(.+?)"""")
-
-    //language=RegExp
-    private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
-
-    override fun intercept(chain: Interceptor.Chain): Response {
-        val response = chain.proceed(chain.request())
-
-        // Check if we already solved a challenge
-        if (response.code() != 503 &&
-                cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
-            return response
-        }
-
-        // Check if Cloudflare anti-bot is on
-        if ("URL=/cdn-cgi/" in response.header("Refresh", "")
-                && response.header("Server", "") == "cloudflare-nginx") {
-            return chain.proceed(resolveChallenge(response))
-        }
-
-        return response
-    }
-
-    private fun resolveChallenge(response: Response): Request {
-        val duktape = Duktape.create()
-        try {
-            val originalRequest = response.request()
-            val domain = originalRequest.url().host()
-            val content = response.body().string()
-
-            // CloudFlare requires waiting 4 seconds before resolving the challenge
-            Thread.sleep(4000)
-
-            val operation = operationPattern.find(content)?.groups?.get(1)?.value
-            val challenge = challengePattern.find(content)?.groups?.get(1)?.value
-            val pass = passPattern.find(content)?.groups?.get(1)?.value
-
-            if (operation == null || challenge == null || pass == null) {
-                throw RuntimeException("Failed resolving Cloudflare challenge")
-            }
-
-            val js = operation
-                    //language=RegExp
-                    .replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
-                    //language=RegExp
-                    .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
-                    .replace("\n", "")
-
-            val result = (duktape.evaluate(js) as Double).toInt()
-
-            val answer = "${result + domain.length}"
-
-            val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
-                    .addQueryParameter("jschl_vc", challenge)
-                    .addQueryParameter("pass", pass)
-                    .addQueryParameter("jschl_answer", answer)
-                    .toString()
-
-            val referer = originalRequest.url().toString()
-            return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
-        } finally {
-            duktape.close()
-        }
-    }
-
+package eu.kanade.tachiyomi.network
+
+import com.squareup.duktape.Duktape
+import okhttp3.HttpUrl
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+
+class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
+
+    //language=RegExp
+    private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
+    
+    //language=RegExp
+    private val passPattern = Regex("""name="pass" value="(.+?)"""")
+
+    //language=RegExp
+    private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val response = chain.proceed(chain.request())
+
+        // Check if we already solved a challenge
+        if (response.code() != 503 &&
+                cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
+            return response
+        }
+
+        // Check if Cloudflare anti-bot is on
+        if ("URL=/cdn-cgi/" in response.header("Refresh", "")
+                && response.header("Server", "") == "cloudflare-nginx") {
+            return chain.proceed(resolveChallenge(response))
+        }
+
+        return response
+    }
+
+    private fun resolveChallenge(response: Response): Request {
+        val duktape = Duktape.create()
+        try {
+            val originalRequest = response.request()
+            val domain = originalRequest.url().host()
+            val content = response.body().string()
+
+            // CloudFlare requires waiting 4 seconds before resolving the challenge
+            Thread.sleep(4000)
+
+            val operation = operationPattern.find(content)?.groups?.get(1)?.value
+            val challenge = challengePattern.find(content)?.groups?.get(1)?.value
+            val pass = passPattern.find(content)?.groups?.get(1)?.value
+
+            if (operation == null || challenge == null || pass == null) {
+                throw RuntimeException("Failed resolving Cloudflare challenge")
+            }
+
+            val js = operation
+                    //language=RegExp
+                    .replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
+                    //language=RegExp
+                    .replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
+                    .replace("\n", "")
+
+            val result = (duktape.evaluate(js) as Double).toInt()
+
+            val answer = "${result + domain.length}"
+
+            val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
+                    .addQueryParameter("jschl_vc", challenge)
+                    .addQueryParameter("pass", pass)
+                    .addQueryParameter("jschl_answer", answer)
+                    .toString()
+
+            val referer = originalRequest.url().toString()
+            return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
+        } finally {
+            duktape.close()
+        }
+    }
+
 }

+ 38 - 38
app/src/main/java/eu/kanade/tachiyomi/data/network/NetworkHelper.kt → app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt

@@ -1,38 +1,38 @@
-package eu.kanade.tachiyomi.data.network
-
-import android.content.Context
-import okhttp3.Cache
-import okhttp3.OkHttpClient
-import java.io.File
-
-class NetworkHelper(context: Context) {
-
-    private val cacheDir = File(context.cacheDir, "network_cache")
-
-    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
-
-    private val cookieManager = PersistentCookieJar(context)
-
-    val client = OkHttpClient.Builder()
-            .cookieJar(cookieManager)
-            .cache(Cache(cacheDir, cacheSize))
-            .build()
-
-    val forceCacheClient = client.newBuilder()
-            .addNetworkInterceptor { chain ->
-                val originalResponse = chain.proceed(chain.request())
-                originalResponse.newBuilder()
-                        .removeHeader("Pragma")
-                        .header("Cache-Control", "max-age=600")
-                        .build()
-            }
-            .build()
-
-    val cloudflareClient = client.newBuilder()
-            .addInterceptor(CloudflareInterceptor(cookies))
-            .build()
-
-    val cookies: PersistentCookieStore
-        get() = cookieManager.store
-
-}
+package eu.kanade.tachiyomi.network
+
+import android.content.Context
+import okhttp3.Cache
+import okhttp3.OkHttpClient
+import java.io.File
+
+class NetworkHelper(context: Context) {
+
+    private val cacheDir = File(context.cacheDir, "network_cache")
+
+    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
+
+    private val cookieManager = PersistentCookieJar(context)
+
+    val client = OkHttpClient.Builder()
+            .cookieJar(cookieManager)
+            .cache(Cache(cacheDir, cacheSize))
+            .build()
+
+    val forceCacheClient = client.newBuilder()
+            .addNetworkInterceptor { chain ->
+                val originalResponse = chain.proceed(chain.request())
+                originalResponse.newBuilder()
+                        .removeHeader("Pragma")
+                        .header("Cache-Control", "max-age=600")
+                        .build()
+            }
+            .build()
+
+    val cloudflareClient = client.newBuilder()
+            .addInterceptor(CloudflareInterceptor(cookies))
+            .build()
+
+    val cookies: PersistentCookieStore
+        get() = cookieManager.store
+
+}

+ 69 - 69
app/src/main/java/eu/kanade/tachiyomi/data/network/OkHttpExtensions.kt → app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt

@@ -1,70 +1,70 @@
-package eu.kanade.tachiyomi.data.network
-
-import okhttp3.Call
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import rx.Observable
-import rx.Producer
-import rx.Subscription
-import java.util.concurrent.atomic.AtomicBoolean
-
-fun Call.asObservable(): Observable<Response> {
-    return Observable.create { subscriber ->
-        // Since Call is a one-shot type, clone it for each new subscriber.
-        val call = clone()
-
-        // Wrap the call in a helper which handles both unsubscription and backpressure.
-        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
-            override fun request(n: Long) {
-                if (n == 0L || !compareAndSet(false, true)) return
-
-                try {
-                    val response = call.execute()
-                    if (!subscriber.isUnsubscribed) {
-                        subscriber.onNext(response)
-                        subscriber.onCompleted()
-                    }
-                } catch (error: Exception) {
-                    if (!subscriber.isUnsubscribed) {
-                        subscriber.onError(error)
-                    }
-                }
-            }
-
-            override fun unsubscribe() {
-                call.cancel()
-            }
-
-            override fun isUnsubscribed(): Boolean {
-                return call.isCanceled
-            }
-        }
-
-        subscriber.add(requestArbiter)
-        subscriber.setProducer(requestArbiter)
-    }
-}
-
-fun Call.asObservableSuccess(): Observable<Response> {
-    return asObservable().doOnNext { response ->
-        if (!response.isSuccessful) {
-            response.close()
-            throw Exception("HTTP error ${response.code()}")
-        }
-    }
-}
-
-fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
-    val progressClient = newBuilder()
-            .cache(null)
-            .addNetworkInterceptor { chain ->
-                val originalResponse = chain.proceed(chain.request())
-                originalResponse.newBuilder()
-                        .body(ProgressResponseBody(originalResponse.body(), listener))
-                        .build()
-            }
-            .build()
-
-    return progressClient.newCall(request)
+package eu.kanade.tachiyomi.network
+
+import okhttp3.Call
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import rx.Producer
+import rx.Subscription
+import java.util.concurrent.atomic.AtomicBoolean
+
+fun Call.asObservable(): Observable<Response> {
+    return Observable.create { subscriber ->
+        // Since Call is a one-shot type, clone it for each new subscriber.
+        val call = clone()
+
+        // Wrap the call in a helper which handles both unsubscription and backpressure.
+        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
+            override fun request(n: Long) {
+                if (n == 0L || !compareAndSet(false, true)) return
+
+                try {
+                    val response = call.execute()
+                    if (!subscriber.isUnsubscribed) {
+                        subscriber.onNext(response)
+                        subscriber.onCompleted()
+                    }
+                } catch (error: Exception) {
+                    if (!subscriber.isUnsubscribed) {
+                        subscriber.onError(error)
+                    }
+                }
+            }
+
+            override fun unsubscribe() {
+                call.cancel()
+            }
+
+            override fun isUnsubscribed(): Boolean {
+                return call.isCanceled
+            }
+        }
+
+        subscriber.add(requestArbiter)
+        subscriber.setProducer(requestArbiter)
+    }
+}
+
+fun Call.asObservableSuccess(): Observable<Response> {
+    return asObservable().doOnNext { response ->
+        if (!response.isSuccessful) {
+            response.close()
+            throw Exception("HTTP error ${response.code()}")
+        }
+    }
+}
+
+fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
+    val progressClient = newBuilder()
+            .cache(null)
+            .addNetworkInterceptor { chain ->
+                val originalResponse = chain.proceed(chain.request())
+                originalResponse.newBuilder()
+                        .body(ProgressResponseBody(originalResponse.body(), listener))
+                        .build()
+            }
+            .build()
+
+    return progressClient.newCall(request)
 }

+ 18 - 18
app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieJar.kt → app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieJar.kt

@@ -1,19 +1,19 @@
-package eu.kanade.tachiyomi.data.network
-
-import android.content.Context
-import okhttp3.Cookie
-import okhttp3.CookieJar
-import okhttp3.HttpUrl
-
-class PersistentCookieJar(context: Context) : CookieJar {
-
-    val store = PersistentCookieStore(context)
-
-    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
-        store.addAll(url, cookies)
-    }
-
-    override fun loadForRequest(url: HttpUrl): List<Cookie> {
-        return store.get(url)
-    }
+package eu.kanade.tachiyomi.network
+
+import android.content.Context
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.HttpUrl
+
+class PersistentCookieJar(context: Context) : CookieJar {
+
+    val store = PersistentCookieStore(context)
+
+    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
+        store.addAll(url, cookies)
+    }
+
+    override fun loadForRequest(url: HttpUrl): List<Cookie> {
+        return store.get(url)
+    }
 }

+ 74 - 74
app/src/main/java/eu/kanade/tachiyomi/data/network/PersistentCookieStore.kt → app/src/main/java/eu/kanade/tachiyomi/network/PersistentCookieStore.kt

@@ -1,75 +1,75 @@
-package eu.kanade.tachiyomi.data.network
-
-import android.content.Context
-import okhttp3.Cookie
-import okhttp3.HttpUrl
-import java.net.URI
-import java.util.concurrent.ConcurrentHashMap
-
-class PersistentCookieStore(context: Context) {
-
-    private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
-    private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
-
-    init {
-        for ((key, value) in prefs.all) {
-            @Suppress("UNCHECKED_CAST")
-            val cookies = value as? Set<String>
-            if (cookies != null) {
-                try {
-                    val url = HttpUrl.parse("http://$key")
-                    val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
-                            .filter { !it.hasExpired() }
-                    cookieMap.put(key, nonExpiredCookies)
-                } catch (e: Exception) {
-                    // Ignore
-                }
-            }
-        }
-    }
-
-    fun addAll(url: HttpUrl, cookies: List<Cookie>) {
-        synchronized(this) {
-            val key = url.uri().host
-
-            // Append or replace the cookies for this domain.
-            val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
-            for (cookie in cookies) {
-                // Find a cookie with the same name. Replace it if found, otherwise add a new one.
-                val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
-                if (pos == -1) {
-                    cookiesForDomain.add(cookie)
-                } else {
-                    cookiesForDomain[pos] = cookie
-                }
-            }
-            cookieMap.put(key, cookiesForDomain)
-
-            // Get cookies to be stored in disk
-            val newValues = cookiesForDomain.asSequence()
-                    .filter { it.persistent() && !it.hasExpired() }
-                    .map { it.toString() }
-                    .toSet()
-
-            prefs.edit().putStringSet(key, newValues).apply()
-        }
-    }
-
-    fun removeAll() {
-        synchronized(this) {
-            prefs.edit().clear().apply()
-            cookieMap.clear()
-        }
-    }
-
-    fun get(url: HttpUrl) = get(url.uri().host)
-
-    fun get(uri: URI) = get(uri.host)
-
-    private fun get(url: String): List<Cookie> {
-        return cookieMap[url].orEmpty().filter { !it.hasExpired() }
-    }
-
-    private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
-
+package eu.kanade.tachiyomi.network
+
+import android.content.Context
+import okhttp3.Cookie
+import okhttp3.HttpUrl
+import java.net.URI
+import java.util.concurrent.ConcurrentHashMap
+
+class PersistentCookieStore(context: Context) {
+
+    private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
+    private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
+
+    init {
+        for ((key, value) in prefs.all) {
+            @Suppress("UNCHECKED_CAST")
+            val cookies = value as? Set<String>
+            if (cookies != null) {
+                try {
+                    val url = HttpUrl.parse("http://$key")
+                    val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
+                            .filter { !it.hasExpired() }
+                    cookieMap.put(key, nonExpiredCookies)
+                } catch (e: Exception) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    fun addAll(url: HttpUrl, cookies: List<Cookie>) {
+        synchronized(this) {
+            val key = url.uri().host
+
+            // Append or replace the cookies for this domain.
+            val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
+            for (cookie in cookies) {
+                // Find a cookie with the same name. Replace it if found, otherwise add a new one.
+                val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
+                if (pos == -1) {
+                    cookiesForDomain.add(cookie)
+                } else {
+                    cookiesForDomain[pos] = cookie
+                }
+            }
+            cookieMap.put(key, cookiesForDomain)
+
+            // Get cookies to be stored in disk
+            val newValues = cookiesForDomain.asSequence()
+                    .filter { it.persistent() && !it.hasExpired() }
+                    .map { it.toString() }
+                    .toSet()
+
+            prefs.edit().putStringSet(key, newValues).apply()
+        }
+    }
+
+    fun removeAll() {
+        synchronized(this) {
+            prefs.edit().clear().apply()
+            cookieMap.clear()
+        }
+    }
+
+    fun get(url: HttpUrl) = get(url.uri().host)
+
+    fun get(uri: URI) = get(uri.host)
+
+    private fun get(url: String): List<Cookie> {
+        return cookieMap[url].orEmpty().filter { !it.hasExpired() }
+    }
+
+    private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
+
 }

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressListener.kt → app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt

@@ -1,5 +1,5 @@
-package eu.kanade.tachiyomi.data.network
-
-interface ProgressListener {
-    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
+package eu.kanade.tachiyomi.network
+
+interface ProgressListener {
+    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
 }

+ 39 - 39
app/src/main/java/eu/kanade/tachiyomi/data/network/ProgressResponseBody.kt → app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt

@@ -1,40 +1,40 @@
-package eu.kanade.tachiyomi.data.network
-
-import okhttp3.MediaType
-import okhttp3.ResponseBody
-import okio.*
-import java.io.IOException
-
-class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
-
-    private val bufferedSource: BufferedSource by lazy {
-        Okio.buffer(source(responseBody.source()))
-    }
-
-    override fun contentType(): MediaType {
-        return responseBody.contentType()
-    }
-
-    override fun contentLength(): Long {
-        return responseBody.contentLength()
-    }
-
-    override fun source(): BufferedSource {
-        return bufferedSource
-    }
-
-    private fun source(source: Source): Source {
-        return object : ForwardingSource(source) {
-            internal var totalBytesRead = 0L
-
-            @Throws(IOException::class)
-            override fun read(sink: Buffer, byteCount: Long): Long {
-                val bytesRead = super.read(sink, byteCount)
-                // read() returns the number of bytes read, or -1 if this source is exhausted.
-                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
-                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
-                return bytesRead
-            }
-        }
-    }
+package eu.kanade.tachiyomi.network
+
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import okio.*
+import java.io.IOException
+
+class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
+
+    private val bufferedSource: BufferedSource by lazy {
+        Okio.buffer(source(responseBody.source()))
+    }
+
+    override fun contentType(): MediaType {
+        return responseBody.contentType()
+    }
+
+    override fun contentLength(): Long {
+        return responseBody.contentLength()
+    }
+
+    override fun source(): BufferedSource {
+        return bufferedSource
+    }
+
+    private fun source(source: Source): Source {
+        return object : ForwardingSource(source) {
+            internal var totalBytesRead = 0L
+
+            @Throws(IOException::class)
+            override fun read(sink: Buffer, byteCount: Long): Long {
+                val bytesRead = super.read(sink, byteCount)
+                // read() returns the number of bytes read, or -1 if this source is exhausted.
+                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
+                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
+                return bytesRead
+            }
+        }
+    }
 }

+ 32 - 32
app/src/main/java/eu/kanade/tachiyomi/data/network/Requests.kt → app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt

@@ -1,32 +1,32 @@
-package eu.kanade.tachiyomi.data.network
-
-import okhttp3.*
-import java.util.concurrent.TimeUnit.MINUTES
-
-private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
-private val DEFAULT_HEADERS = Headers.Builder().build()
-private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
-
-fun GET(url: String,
-        headers: Headers = DEFAULT_HEADERS,
-        cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
-
-    return Request.Builder()
-            .url(url)
-            .headers(headers)
-            .cacheControl(cache)
-            .build()
-}
-
-fun POST(url: String,
-         headers: Headers = DEFAULT_HEADERS,
-         body: RequestBody = DEFAULT_BODY,
-         cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
-
-    return Request.Builder()
-            .url(url)
-            .post(body)
-            .headers(headers)
-            .cacheControl(cache)
-            .build()
-}
+package eu.kanade.tachiyomi.network
+
+import okhttp3.*
+import java.util.concurrent.TimeUnit.MINUTES
+
+private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
+private val DEFAULT_HEADERS = Headers.Builder().build()
+private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
+
+fun GET(url: String,
+        headers: Headers = DEFAULT_HEADERS,
+        cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+    return Request.Builder()
+            .url(url)
+            .headers(headers)
+            .cacheControl(cache)
+            .build()
+}
+
+fun POST(url: String,
+         headers: Headers = DEFAULT_HEADERS,
+         body: RequestBody = DEFAULT_BODY,
+         cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
+
+    return Request.Builder()
+            .url(url)
+            .post(body)
+            .headers(headers)
+            .cacheControl(cache)
+            .build()
+}

+ 45 - 45
app/src/main/java/eu/kanade/tachiyomi/data/source/CatalogueSource.kt → app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt

@@ -1,46 +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
+package eu.kanade.tachiyomi.source
+
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.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
 }

+ 43 - 43
app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt → app/src/main/java/eu/kanade/tachiyomi/source/Source.kt

@@ -1,44 +1,44 @@
-package eu.kanade.tachiyomi.data.source
-
-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
-
-/**
- * A basic interface for creating a source. It could be an online source, a local source, etc...
- */
-interface Source {
-
-    /**
-     * Id for the source. Must be unique.
-     */
-    val id: Long
-
-    /**
-     * Name of the source.
-     */
-    val name: String
-
-    /**
-     * Returns an observable with the updated details for a manga.
-     *
-     * @param manga the manga to update.
-     */
-    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: SManga): Observable<List<SChapter>>
-
-    /**
-     * Returns an observable with the list of pages a chapter has.
-     *
-     * @param chapter the chapter.
-     */
-    fun fetchPageList(chapter: SChapter): Observable<List<Page>>
-
+package eu.kanade.tachiyomi.source
+
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import rx.Observable
+
+/**
+ * A basic interface for creating a source. It could be an online source, a local source, etc...
+ */
+interface Source {
+
+    /**
+     * Id for the source. Must be unique.
+     */
+    val id: Long
+
+    /**
+     * Name of the source.
+     */
+    val name: String
+
+    /**
+     * Returns an observable with the updated details for a manga.
+     *
+     * @param manga the manga to update.
+     */
+    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: SManga): Observable<List<SChapter>>
+
+    /**
+     * Returns an observable with the list of pages a chapter has.
+     *
+     * @param chapter the chapter.
+     */
+    fun fetchPageList(chapter: SChapter): Observable<List<Page>>
+
 }

+ 159 - 159
app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt → app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt

@@ -1,159 +1,159 @@
-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
-import eu.kanade.tachiyomi.data.source.online.english.*
-import eu.kanade.tachiyomi.data.source.online.german.WieManga
-import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
-import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
-import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
-import eu.kanade.tachiyomi.util.hasPermission
-import org.yaml.snakeyaml.Yaml
-import timber.log.Timber
-import java.io.File
-
-open class SourceManager(private val context: Context) {
-
-    private val sourcesMap = mutableMapOf<Long, Source>()
-
-    init {
-        createSources()
-    }
-
-    open fun get(sourceKey: Long): Source? {
-        return sourcesMap[sourceKey]
-    }
-
-    fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
-
-    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 createYamlSources(): List<Source> {
-        val sources = mutableListOf<Source>()
-
-        val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
-                File.separator + context.getString(R.string.app_name), "parsers")
-
-        if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
-            val yaml = Yaml()
-            for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
-                try {
-                    val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
-                    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
-    }
-
-}
+package eu.kanade.tachiyomi.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.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.online.YamlOnlineSource
+import eu.kanade.tachiyomi.source.online.english.*
+import eu.kanade.tachiyomi.source.online.german.WieManga
+import eu.kanade.tachiyomi.source.online.russian.Mangachan
+import eu.kanade.tachiyomi.source.online.russian.Mintmanga
+import eu.kanade.tachiyomi.source.online.russian.Readmanga
+import eu.kanade.tachiyomi.util.hasPermission
+import org.yaml.snakeyaml.Yaml
+import timber.log.Timber
+import java.io.File
+
+open class SourceManager(private val context: Context) {
+
+    private val sourcesMap = mutableMapOf<Long, Source>()
+
+    init {
+        createSources()
+    }
+
+    open fun get(sourceKey: Long): Source? {
+        return sourcesMap[sourceKey]
+    }
+
+    fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
+
+    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 createYamlSources(): List<Source> {
+        val sources = mutableListOf<Source>()
+
+        val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
+                File.separator + context.getString(R.string.app_name), "parsers")
+
+        if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
+            val yaml = Yaml()
+            for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
+                try {
+                    val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
+                    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
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.source.model
+package eu.kanade.tachiyomi.source.model
 
 sealed class Filter<T>(val name: String, var state: T) {
     open class Header(name: String) : Filter<Any>(name, 0)

+ 6 - 6
app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt

@@ -1,7 +1,7 @@
-package eu.kanade.tachiyomi.data.source.model
-
-data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
-
-    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
-
+package eu.kanade.tachiyomi.source.model
+
+data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
+
+    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
+
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/model/MangasPage.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt

@@ -1,3 +1,3 @@
-package eu.kanade.tachiyomi.data.source.model
-
+package eu.kanade.tachiyomi.source.model
+
 data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

+ 47 - 47
app/src/main/java/eu/kanade/tachiyomi/data/source/model/Page.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt

@@ -1,47 +1,47 @@
-package eu.kanade.tachiyomi.data.source.model
-
-import android.net.Uri
-import eu.kanade.tachiyomi.data.network.ProgressListener
-import eu.kanade.tachiyomi.ui.reader.ReaderChapter
-import rx.subjects.Subject
-
-class Page(
-        val index: Int,
-        val url: String = "",
-        var imageUrl: String? = null,
-        @Transient var uri: Uri? = null
-) : ProgressListener {
-
-    val number: Int
-        get() = index + 1
-
-    @Transient lateinit var chapter: ReaderChapter
-
-    @Transient @Volatile var status: Int = 0
-        set(value) {
-            field = value
-            statusSubject?.onNext(value)
-        }
-
-    @Transient @Volatile var progress: Int = 0
-
-    @Transient private var statusSubject: Subject<Int, Int>? = null
-
-    override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
-        progress = (100 * bytesRead / contentLength).toInt()
-    }
-
-    fun setStatusSubject(subject: Subject<Int, Int>?) {
-        this.statusSubject = subject
-    }
-
-    companion object {
-
-        const val QUEUE = 0
-        const val LOAD_PAGE = 1
-        const val DOWNLOAD_IMAGE = 2
-        const val READY = 3
-        const val ERROR = 4
-    }
-
-}
+package eu.kanade.tachiyomi.source.model
+
+import android.net.Uri
+import eu.kanade.tachiyomi.network.ProgressListener
+import eu.kanade.tachiyomi.ui.reader.ReaderChapter
+import rx.subjects.Subject
+
+class Page(
+        val index: Int,
+        val url: String = "",
+        var imageUrl: String? = null,
+        @Transient var uri: Uri? = null
+) : ProgressListener {
+
+    val number: Int
+        get() = index + 1
+
+    @Transient lateinit var chapter: ReaderChapter
+
+    @Transient @Volatile var status: Int = 0
+        set(value) {
+            field = value
+            statusSubject?.onNext(value)
+        }
+
+    @Transient @Volatile var progress: Int = 0
+
+    @Transient private var statusSubject: Subject<Int, Int>? = null
+
+    override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
+        progress = (100 * bytesRead / contentLength).toInt()
+    }
+
+    fun setStatusSubject(subject: Subject<Int, Int>?) {
+        this.statusSubject = subject
+    }
+
+    companion object {
+
+        const val QUEUE = 0
+        const val LOAD_PAGE = 1
+        const val DOWNLOAD_IMAGE = 2
+        const val READY = 3
+        const val ERROR = 4
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapter.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.source.model
+package eu.kanade.tachiyomi.source.model
 
 import java.io.Serializable
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SChapterImpl.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.source.model
+package eu.kanade.tachiyomi.source.model
 
 class SChapterImpl : SChapter {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SManga.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.source.model
+package eu.kanade.tachiyomi.source.model
 
 import java.io.Serializable
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/model/SMangaImpl.kt → app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaImpl.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.source.model
+package eu.kanade.tachiyomi.source.model
 
 class SMangaImpl : SManga {
 

+ 14 - 14
app/src/main/java/eu/kanade/tachiyomi/data/source/online/LoginSource.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt

@@ -1,15 +1,15 @@
-package eu.kanade.tachiyomi.data.source.online
-
-import eu.kanade.tachiyomi.data.source.Source
-import okhttp3.Response
-import rx.Observable
-
-interface LoginSource : Source {
-
-    fun isLogged(): Boolean
-
-    fun login(username: String, password: String): Observable<Boolean>
-
-    fun isAuthenticationSuccessful(response: Response): Boolean
-
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.Source
+import okhttp3.Response
+import rx.Observable
+
+interface LoginSource : Source {
+
+    fun isLogged(): Boolean
+
+    fun login(username: String, password: String): Observable<Boolean>
+
+    fun isAuthenticationSuccessful(response: Response): Boolean
+
 }

+ 361 - 361
app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/OnlineSource.kt

@@ -1,361 +1,361 @@
-package eu.kanade.tachiyomi.data.source.online
-
-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.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 : CatalogueSource {
-
-    /**
-     * Network service.
-     */
-    val network: NetworkHelper by injectLazy()
-
-    /**
-     * Preferences helper.
-     */
-    val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * Base url of the website without the trailing slash, like: http://mysite.com
-     */
-    abstract val baseUrl: String
-
-    /**
-     * Version id used to generate the source id. If the site completely changes and urls are
-     * incompatible, you may increase this value and it'll be considered as a new source.
-     */
-    open val versionId = 1
-
-    /**
-     * Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
-     * of the MD5 of the string: sourcename/language/versionId
-     * Note the generated id sets the sign bit to 0.
-     */
-    override val id by lazy {
-        val key = "${name.toLowerCase()}/$lang/$versionId"
-        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
-        (0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
-    }
-
-    /**
-     * Headers used for requests.
-     */
-    val headers: Headers by lazy { headersBuilder().build() }
-
-    /**
-     * Default network client for doing requests.
-     */
-    open val client: OkHttpClient
-        get() = network.client
-
-    /**
-     * Headers builder for requests. Implementations can override this method for custom headers.
-     */
-    open protected fun headersBuilder() = Headers.Builder().apply {
-        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
-    }
-
-    /**
-     * Visible name of the source.
-     */
-    override fun toString() = "$name (${lang.toUpperCase()})"
-
-    /**
-     * Returns an observable containing a page with a list of manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param page the page number to retrieve.
-     */
-    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
-        return client.newCall(popularMangaRequest(page))
-                .asObservableSuccess()
-                .map { response ->
-                    popularMangaParse(response)
-                }
-    }
-
-    /**
-     * Returns the request for the popular manga given the page.
-     *
-     * @param page the page number to retrieve.
-     */
-    abstract protected fun popularMangaRequest(page: Int): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    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 number to retrieve.
-     * @param query the search query.
-     * @param filters the list of filters to apply.
-     */
-    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 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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun searchMangaParse(response: Response): MangasPage
-
-    /**
-     * Returns an observable containing a page with a list of latest manga updates.
-     *
-     * @param page the page number to retrieve.
-     */
-    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.
-     */
-    abstract protected fun latestUpdatesRequest(page: Int): Request
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun latestUpdatesParse(response: Response): MangasPage
-
-    /**
-     * Returns an observable with the updated details for a manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param manga the manga to be updated.
-     */
-    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
-        return client.newCall(mangaDetailsRequest(manga))
-                .asObservableSuccess()
-                .map { response ->
-                    mangaDetailsParse(response).apply { initialized = true }
-                }
-    }
-
-    /**
-     * Returns the request for the details of a manga. Override only if it's needed to change the
-     * url, send different headers or request method like POST.
-     *
-     * @param manga the manga to be updated.
-     */
-    open fun mangaDetailsRequest(manga: SManga): Request {
-        return GET(baseUrl + manga.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns the details of a manga.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun mangaDetailsParse(response: Response): SManga
-
-    /**
-     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
-     * override this method.
-     *
-     * @param manga the manga to look for chapters.
-     */
-    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
-     * the url, send different headers or request method like POST.
-     *
-     * @param manga the manga to look for chapters.
-     */
-    open protected fun chapterListRequest(manga: SManga): Request {
-        return GET(baseUrl + manga.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns a list of chapters.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun chapterListParse(response: Response): List<SChapter>
-
-    /**
-     * Returns an observable with the page list for a chapter.
-     *
-     * @param chapter the chapter whose page list has to be fetched.
-     */
-    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.
-     */
-    open protected fun pageListRequest(chapter: SChapter): Request {
-        return GET(baseUrl + chapter.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns a list of pages.
-     *
-     * @param response the response from the site.
-     */
-    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
-     * error, it will return null instead of throwing an exception.
-     *
-     * @param page the page whose source image has to be fetched.
-     */
-    open fun fetchImageUrl(page: Page): Observable<String> {
-        return client.newCall(imageUrlRequest(page))
-                .asObservableSuccess()
-                .map { imageUrlParse(it) }
-    }
-
-    /**
-     * Returns the request for getting the url to the source image. Override only if it's needed to
-     * override the url, send different headers or request method like POST.
-     *
-     * @param page the chapter whose page list has to be fetched
-     */
-    open protected fun imageUrlRequest(page: Page): Request {
-        return GET(page.url, headers)
-    }
-
-    /**
-     * Parses the response from the site and returns the absolute url to the source image.
-     *
-     * @param response the response from the site.
-     */
-    abstract protected fun imageUrlParse(response: Response): String
-
-    /**
-     * Returns an observable with the response of the source image.
-     *
-     * @param page the page whose source image has to be downloaded.
-     */
-    fun fetchImage(page: Page): Observable<Response> {
-        return client.newCallWithProgress(imageRequest(page), page)
-                .asObservableSuccess()
-    }
-
-    /**
-     * Returns the request for getting the source image. Override only if it's needed to override
-     * the url, send different headers or request method like POST.
-     *
-     * @param page the chapter whose page list has to be fetched
-     */
-    open protected fun imageRequest(page: Page): Request {
-        return GET(page.imageUrl!!, headers)
-    }
-
-    /**
-     * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
-     * database and the urls could still work after a domain change.
-     *
-     * @param url the full url to the chapter.
-     */
-    fun SChapter.setUrlWithoutDomain(url: String) {
-        this.url = getUrlWithoutDomain(url)
-    }
-
-    /**
-     * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
-     * database and the urls could still work after a domain change.
-     *
-     * @param url the full url to the manga.
-     */
-    fun SManga.setUrlWithoutDomain(url: String) {
-        this.url = getUrlWithoutDomain(url)
-    }
-
-    /**
-     * Returns the url of the given string without the scheme and domain.
-     *
-     * @param orig the full url.
-     */
-    private fun getUrlWithoutDomain(orig: String): String {
-        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
-        }
-    }
-
-    /**
-     * Called before inserting a new chapter into database. Use it if you need to override chapter
-     * fields, like the title or the chapter number. Do not change anything to [manga].
-     *
-     * @param chapter the chapter to be added.
-     * @param manga the manga of the chapter.
-     */
-    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
-    }
-
-    /**
-     * Returns the list of filters for the source.
-     */
-    override fun getFilterList() = FilterList()
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.network.newCallWithProgress
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.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 : CatalogueSource {
+
+    /**
+     * Network service.
+     */
+    val network: NetworkHelper by injectLazy()
+
+    /**
+     * Preferences helper.
+     */
+    val preferences: PreferencesHelper by injectLazy()
+
+    /**
+     * Base url of the website without the trailing slash, like: http://mysite.com
+     */
+    abstract val baseUrl: String
+
+    /**
+     * Version id used to generate the source id. If the site completely changes and urls are
+     * incompatible, you may increase this value and it'll be considered as a new source.
+     */
+    open val versionId = 1
+
+    /**
+     * Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
+     * of the MD5 of the string: sourcename/language/versionId
+     * Note the generated id sets the sign bit to 0.
+     */
+    override val id by lazy {
+        val key = "${name.toLowerCase()}/$lang/$versionId"
+        val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
+        (0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
+    }
+
+    /**
+     * Headers used for requests.
+     */
+    val headers: Headers by lazy { headersBuilder().build() }
+
+    /**
+     * Default network client for doing requests.
+     */
+    open val client: OkHttpClient
+        get() = network.client
+
+    /**
+     * Headers builder for requests. Implementations can override this method for custom headers.
+     */
+    open protected fun headersBuilder() = Headers.Builder().apply {
+        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
+    }
+
+    /**
+     * Visible name of the source.
+     */
+    override fun toString() = "$name (${lang.toUpperCase()})"
+
+    /**
+     * Returns an observable containing a page with a list of manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param page the page number to retrieve.
+     */
+    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
+        return client.newCall(popularMangaRequest(page))
+                .asObservableSuccess()
+                .map { response ->
+                    popularMangaParse(response)
+                }
+    }
+
+    /**
+     * Returns the request for the popular manga given the page.
+     *
+     * @param page the page number to retrieve.
+     */
+    abstract protected fun popularMangaRequest(page: Int): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    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 number to retrieve.
+     * @param query the search query.
+     * @param filters the list of filters to apply.
+     */
+    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 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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun searchMangaParse(response: Response): MangasPage
+
+    /**
+     * Returns an observable containing a page with a list of latest manga updates.
+     *
+     * @param page the page number to retrieve.
+     */
+    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.
+     */
+    abstract protected fun latestUpdatesRequest(page: Int): Request
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun latestUpdatesParse(response: Response): MangasPage
+
+    /**
+     * Returns an observable with the updated details for a manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param manga the manga to be updated.
+     */
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        return client.newCall(mangaDetailsRequest(manga))
+                .asObservableSuccess()
+                .map { response ->
+                    mangaDetailsParse(response).apply { initialized = true }
+                }
+    }
+
+    /**
+     * Returns the request for the details of a manga. Override only if it's needed to change the
+     * url, send different headers or request method like POST.
+     *
+     * @param manga the manga to be updated.
+     */
+    open fun mangaDetailsRequest(manga: SManga): Request {
+        return GET(baseUrl + manga.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns the details of a manga.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun mangaDetailsParse(response: Response): SManga
+
+    /**
+     * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
+     * override this method.
+     *
+     * @param manga the manga to look for chapters.
+     */
+    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
+     * the url, send different headers or request method like POST.
+     *
+     * @param manga the manga to look for chapters.
+     */
+    open protected fun chapterListRequest(manga: SManga): Request {
+        return GET(baseUrl + manga.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns a list of chapters.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun chapterListParse(response: Response): List<SChapter>
+
+    /**
+     * Returns an observable with the page list for a chapter.
+     *
+     * @param chapter the chapter whose page list has to be fetched.
+     */
+    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.
+     */
+    open protected fun pageListRequest(chapter: SChapter): Request {
+        return GET(baseUrl + chapter.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns a list of pages.
+     *
+     * @param response the response from the site.
+     */
+    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
+     * error, it will return null instead of throwing an exception.
+     *
+     * @param page the page whose source image has to be fetched.
+     */
+    open fun fetchImageUrl(page: Page): Observable<String> {
+        return client.newCall(imageUrlRequest(page))
+                .asObservableSuccess()
+                .map { imageUrlParse(it) }
+    }
+
+    /**
+     * Returns the request for getting the url to the source image. Override only if it's needed to
+     * override the url, send different headers or request method like POST.
+     *
+     * @param page the chapter whose page list has to be fetched
+     */
+    open protected fun imageUrlRequest(page: Page): Request {
+        return GET(page.url, headers)
+    }
+
+    /**
+     * Parses the response from the site and returns the absolute url to the source image.
+     *
+     * @param response the response from the site.
+     */
+    abstract protected fun imageUrlParse(response: Response): String
+
+    /**
+     * Returns an observable with the response of the source image.
+     *
+     * @param page the page whose source image has to be downloaded.
+     */
+    fun fetchImage(page: Page): Observable<Response> {
+        return client.newCallWithProgress(imageRequest(page), page)
+                .asObservableSuccess()
+    }
+
+    /**
+     * Returns the request for getting the source image. Override only if it's needed to override
+     * the url, send different headers or request method like POST.
+     *
+     * @param page the chapter whose page list has to be fetched
+     */
+    open protected fun imageRequest(page: Page): Request {
+        return GET(page.imageUrl!!, headers)
+    }
+
+    /**
+     * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
+     * database and the urls could still work after a domain change.
+     *
+     * @param url the full url to the chapter.
+     */
+    fun SChapter.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
+    }
+
+    /**
+     * Assigns the url of the manga without the scheme and domain. It saves some redundancy from
+     * database and the urls could still work after a domain change.
+     *
+     * @param url the full url to the manga.
+     */
+    fun SManga.setUrlWithoutDomain(url: String) {
+        this.url = getUrlWithoutDomain(url)
+    }
+
+    /**
+     * Returns the url of the given string without the scheme and domain.
+     *
+     * @param orig the full url.
+     */
+    private fun getUrlWithoutDomain(orig: String): String {
+        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
+        }
+    }
+
+    /**
+     * Called before inserting a new chapter into database. Use it if you need to override chapter
+     * fields, like the title or the chapter number. Do not change anything to [manga].
+     *
+     * @param chapter the chapter to be added.
+     * @param manga the manga of the chapter.
+     */
+    open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
+    }
+
+    /**
+     * Returns the list of filters for the source.
+     */
+    override fun getFilterList() = FilterList()
+}

+ 98 - 98
app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSourceFetcher.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/OnlineSourceFetcher.kt

@@ -1,98 +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) }
-}
+package eu.kanade.tachiyomi.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.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) }
+}

+ 200 - 200
app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedOnlineSource.kt

@@ -1,200 +1,200 @@
-package eu.kanade.tachiyomi.data.source.online
-
-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
-import org.jsoup.nodes.Element
-
-/**
- * A simple implementation for sources from a website using Jsoup, an HTML parser.
- */
-abstract class ParsedOnlineSource : OnlineSource() {
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun popularMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(popularMangaSelector()).map { element ->
-            popularMangaFromElement(element)
-        }
-
-        val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
-            document.select(selector).first()
-        } != null
-
-        return MangasPage(mangas, hasNextPage)
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun popularMangaSelector(): String
-
-    /**
-     * 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].
-     */
-    abstract protected fun popularMangaFromElement(element: Element): SManga
-
-    /**
-     * 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 popularMangaNextPageSelector(): String?
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun searchMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(searchMangaSelector()).map { element ->
-            searchMangaFromElement(element)
-        }
-
-        val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
-            document.select(selector).first()
-        } != null
-
-        return MangasPage(mangas, hasNextPage)
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun searchMangaSelector(): String
-
-    /**
-     * 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].
-     */
-    abstract protected fun searchMangaFromElement(element: Element): SManga
-
-    /**
-     * 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 searchMangaNextPageSelector(): String?
-
-    /**
-     * Parses the response from the site and returns a [MangasPage] object.
-     *
-     * @param response the response from the site.
-     */
-    override fun latestUpdatesParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(latestUpdatesSelector()).map { element ->
-            latestUpdatesFromElement(element)
-        }
-
-        val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
-            document.select(selector).first()
-        } != null
-
-        return MangasPage(mangas, hasNextPage)
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
-     */
-    abstract protected fun latestUpdatesSelector(): String
-
-    /**
-     * 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): SManga
-
-    /**
-     * 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?
-
-    /**
-     * Parses the response from the site and returns the details of a manga.
-     *
-     * @param response the response from the site.
-     */
-    override fun mangaDetailsParse(response: Response): SManga {
-        return mangaDetailsParse(response.asJsoup())
-    }
-
-    /**
-     * Returns the details of the manga from the given [document].
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun mangaDetailsParse(document: Document): SManga
-
-    /**
-     * Parses the response from the site and returns a list of chapters.
-     *
-     * @param response the response from the site.
-     */
-    override fun chapterListParse(response: Response): List<SChapter> {
-        val document = response.asJsoup()
-        return document.select(chapterListSelector()).map { chapterFromElement(it) }
-    }
-
-    /**
-     * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
-     */
-    abstract protected fun chapterListSelector(): String
-
-    /**
-     * Returns a chapter from the given element.
-     *
-     * @param element an element obtained from [chapterListSelector].
-     */
-    abstract protected fun chapterFromElement(element: Element): SChapter
-
-    /**
-     * Parses the response from the site and returns the page list.
-     *
-     * @param response the response from the site.
-     */
-    override fun pageListParse(response: Response): List<Page> {
-        return pageListParse(response.asJsoup())
-    }
-
-    /**
-     * Returns a page list from the given document.
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun pageListParse(document: Document): List<Page>
-
-    /**
-     * Parse the response from the site and returns the absolute url to the source image.
-     *
-     * @param response the response from the site.
-     */
-    override fun imageUrlParse(response: Response): String {
-        return imageUrlParse(response.asJsoup())
-    }
-
-    /**
-     * Returns the absolute url to the source image from the document.
-     *
-     * @param document the parsed document.
-     */
-    abstract protected fun imageUrlParse(document: Document): String
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+/**
+ * A simple implementation for sources from a website using Jsoup, an HTML parser.
+ */
+abstract class ParsedOnlineSource : OnlineSource() {
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun popularMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(popularMangaSelector()).map { element ->
+            popularMangaFromElement(element)
+        }
+
+        val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun popularMangaSelector(): String
+
+    /**
+     * 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].
+     */
+    abstract protected fun popularMangaFromElement(element: Element): SManga
+
+    /**
+     * 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 popularMangaNextPageSelector(): String?
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun searchMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(searchMangaSelector()).map { element ->
+            searchMangaFromElement(element)
+        }
+
+        val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun searchMangaSelector(): String
+
+    /**
+     * 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].
+     */
+    abstract protected fun searchMangaFromElement(element: Element): SManga
+
+    /**
+     * 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 searchMangaNextPageSelector(): String?
+
+    /**
+     * Parses the response from the site and returns a [MangasPage] object.
+     *
+     * @param response the response from the site.
+     */
+    override fun latestUpdatesParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(latestUpdatesSelector()).map { element ->
+            latestUpdatesFromElement(element)
+        }
+
+        val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
+            document.select(selector).first()
+        } != null
+
+        return MangasPage(mangas, hasNextPage)
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
+     */
+    abstract protected fun latestUpdatesSelector(): String
+
+    /**
+     * 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): SManga
+
+    /**
+     * 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?
+
+    /**
+     * Parses the response from the site and returns the details of a manga.
+     *
+     * @param response the response from the site.
+     */
+    override fun mangaDetailsParse(response: Response): SManga {
+        return mangaDetailsParse(response.asJsoup())
+    }
+
+    /**
+     * Returns the details of the manga from the given [document].
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun mangaDetailsParse(document: Document): SManga
+
+    /**
+     * Parses the response from the site and returns a list of chapters.
+     *
+     * @param response the response from the site.
+     */
+    override fun chapterListParse(response: Response): List<SChapter> {
+        val document = response.asJsoup()
+        return document.select(chapterListSelector()).map { chapterFromElement(it) }
+    }
+
+    /**
+     * Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
+     */
+    abstract protected fun chapterListSelector(): String
+
+    /**
+     * Returns a chapter from the given element.
+     *
+     * @param element an element obtained from [chapterListSelector].
+     */
+    abstract protected fun chapterFromElement(element: Element): SChapter
+
+    /**
+     * Parses the response from the site and returns the page list.
+     *
+     * @param response the response from the site.
+     */
+    override fun pageListParse(response: Response): List<Page> {
+        return pageListParse(response.asJsoup())
+    }
+
+    /**
+     * Returns a page list from the given document.
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun pageListParse(document: Document): List<Page>
+
+    /**
+     * Parse the response from the site and returns the absolute url to the source image.
+     *
+     * @param response the response from the site.
+     */
+    override fun imageUrlParse(response: Response): String {
+        return imageUrlParse(response.asJsoup())
+    }
+
+    /**
+     * Returns the absolute url to the source image from the document.
+     *
+     * @param document the parsed document.
+     */
+    abstract protected fun imageUrlParse(document: Document): String
+}

+ 232 - 232
app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/YamlOnlineSource.kt

@@ -1,232 +1,232 @@
-package eu.kanade.tachiyomi.data.source.online
-
-import eu.kanade.tachiyomi.data.network.GET
-import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.*
-import eu.kanade.tachiyomi.util.asJsoup
-import eu.kanade.tachiyomi.util.attrOrText
-import okhttp3.Request
-import okhttp3.Response
-import org.jsoup.Jsoup
-import org.jsoup.nodes.Element
-import java.text.SimpleDateFormat
-import java.util.*
-
-class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
-
-    val map = YamlSourceNode(mappings)
-
-    override val name: String
-        get() = map.name
-
-    override val baseUrl = map.host.let {
-        if (it.endsWith("/")) it.dropLast(1) else it
-    }
-
-    override val lang = map.lang.toLowerCase()
-
-    override val supportsLatest = map.latestupdates != null
-
-    override val client = when (map.client) {
-        "cloudflare" -> network.cloudflareClient
-        else -> network.client
-    }
-
-    override val id = map.id.let {
-        (it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
-    }
-
-    // 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(url, headers, map.popular.createForm())
-            else -> GET(url, headers)
-        }
-    }
-
-    override fun popularMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(map.popular.manga_css).map { element ->
-            SManga.create().apply {
-                title = element.text()
-                setUrlWithoutDomain(element.attr("href"))
-            }
-        }
-
-        popularNextPage = map.popular.next_url_css?.let { selector ->
-             document.select(selector).first()?.absUrl("href")
-        }
-
-        return MangasPage(mangas, popularNextPage != null)
-    }
-
-    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(url, headers, map.search.createForm())
-            else -> GET(url, headers)
-        }
-    }
-
-    override fun searchMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(map.search.manga_css).map { element ->
-            SManga.create().apply {
-                title = element.text()
-                setUrlWithoutDomain(element.attr("href"))
-            }
-        }
-
-        searchNextPage = map.search.next_url_css?.let { selector ->
-            document.select(selector).first()?.absUrl("href")
-        }
-
-        return MangasPage(mangas, searchNextPage != null)
-    }
-
-    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(url, headers, map.latestupdates.createForm())
-            else -> GET(url, headers)
-        }
-    }
-
-    override fun latestUpdatesParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-
-        val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
-            SManga.create().apply {
-                title = element.text()
-                setUrlWithoutDomain(element.attr("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): SManga {
-        val document = response.asJsoup()
-
-        val manga = SManga.create()
-        with(map.manga) {
-            val pool = parts.get(document)
-
-            manga.author = author?.process(document, pool)
-            manga.artist = artist?.process(document, pool)
-            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) ?: SManga.UNKNOWN
-        }
-        return manga
-    }
-
-    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 = SChapter.create()
-                element.select(title).first().let {
-                    chapter.name = it.text()
-                    chapter.setUrlWithoutDomain(it.attr("href"))
-                }
-                val dateElement = element.select(date?.select).first()
-                chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
-                chapters.add(chapter)
-            }
-        }
-        return chapters
-    }
-
-    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)
-
-        with(map.pages) {
-            // Capture a list of values where page urls will be resolved.
-            val capturedPages = if (pages_regex != null)
-                pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
-            else if (pages_css != null)
-                document.select(pages_css).map { it.attrOrText(pages_attr!!) }
-            else
-                null
-
-            // For each captured value, obtain the url and create a new page.
-            capturedPages?.forEach { value ->
-                // If the captured value isn't an url, we have to use replaces with the chapter url.
-                val pageUrl = if (replace != null && replacement != null)
-                    url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
-                else
-                    value
-
-                pages.add(Page(pages.size, pageUrl))
-            }
-
-            // Capture a list of images.
-            val capturedImages = if (image_regex != null)
-                image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
-            else if (image_css != null)
-                document.select(image_css).map { it.absUrl(image_attr) }
-            else
-                null
-
-            // Assign the image url to each page
-            capturedImages?.forEachIndexed { i, url ->
-                val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
-                page.imageUrl = url
-            }
-        }
-        return pages
-    }
-
-    override fun imageUrlParse(response: Response): String {
-        val body = response.body().string()
-        val url = response.request().url().toString()
-
-        with(map.pages) {
-            return if (image_regex != null)
-                image_regex!!.toRegex().find(body)!!.groups[1]!!.value
-            else if (image_css != null)
-                Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
-            else
-                throw Exception("image_regex and image_css are null")
-        }
-    }
-}
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.util.asJsoup
+import eu.kanade.tachiyomi.util.attrOrText
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.*
+
+class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
+
+    val map = YamlSourceNode(mappings)
+
+    override val name: String
+        get() = map.name
+
+    override val baseUrl = map.host.let {
+        if (it.endsWith("/")) it.dropLast(1) else it
+    }
+
+    override val lang = map.lang.toLowerCase()
+
+    override val supportsLatest = map.latestupdates != null
+
+    override val client = when (map.client) {
+        "cloudflare" -> network.cloudflareClient
+        else -> network.client
+    }
+
+    override val id = map.id.let {
+        (it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
+    }
+
+    // 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(url, headers, map.popular.createForm())
+            else -> GET(url, headers)
+        }
+    }
+
+    override fun popularMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(map.popular.manga_css).map { element ->
+            SManga.create().apply {
+                title = element.text()
+                setUrlWithoutDomain(element.attr("href"))
+            }
+        }
+
+        popularNextPage = map.popular.next_url_css?.let { selector ->
+             document.select(selector).first()?.absUrl("href")
+        }
+
+        return MangasPage(mangas, popularNextPage != null)
+    }
+
+    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(url, headers, map.search.createForm())
+            else -> GET(url, headers)
+        }
+    }
+
+    override fun searchMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(map.search.manga_css).map { element ->
+            SManga.create().apply {
+                title = element.text()
+                setUrlWithoutDomain(element.attr("href"))
+            }
+        }
+
+        searchNextPage = map.search.next_url_css?.let { selector ->
+            document.select(selector).first()?.absUrl("href")
+        }
+
+        return MangasPage(mangas, searchNextPage != null)
+    }
+
+    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(url, headers, map.latestupdates.createForm())
+            else -> GET(url, headers)
+        }
+    }
+
+    override fun latestUpdatesParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+
+        val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
+            SManga.create().apply {
+                title = element.text()
+                setUrlWithoutDomain(element.attr("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): SManga {
+        val document = response.asJsoup()
+
+        val manga = SManga.create()
+        with(map.manga) {
+            val pool = parts.get(document)
+
+            manga.author = author?.process(document, pool)
+            manga.artist = artist?.process(document, pool)
+            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) ?: SManga.UNKNOWN
+        }
+        return manga
+    }
+
+    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 = SChapter.create()
+                element.select(title).first().let {
+                    chapter.name = it.text()
+                    chapter.setUrlWithoutDomain(it.attr("href"))
+                }
+                val dateElement = element.select(date?.select).first()
+                chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
+                chapters.add(chapter)
+            }
+        }
+        return chapters
+    }
+
+    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)
+
+        with(map.pages) {
+            // Capture a list of values where page urls will be resolved.
+            val capturedPages = if (pages_regex != null)
+                pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
+            else if (pages_css != null)
+                document.select(pages_css).map { it.attrOrText(pages_attr!!) }
+            else
+                null
+
+            // For each captured value, obtain the url and create a new page.
+            capturedPages?.forEach { value ->
+                // If the captured value isn't an url, we have to use replaces with the chapter url.
+                val pageUrl = if (replace != null && replacement != null)
+                    url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
+                else
+                    value
+
+                pages.add(Page(pages.size, pageUrl))
+            }
+
+            // Capture a list of images.
+            val capturedImages = if (image_regex != null)
+                image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
+            else if (image_css != null)
+                document.select(image_css).map { it.absUrl(image_attr) }
+            else
+                null
+
+            // Assign the image url to each page
+            capturedImages?.forEachIndexed { i, url ->
+                val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
+                page.imageUrl = url
+            }
+        }
+        return pages
+    }
+
+    override fun imageUrlParse(response: Response): String {
+        val body = response.body().string()
+        val url = response.request().url().toString()
+
+        with(map.pages) {
+            return if (image_regex != null)
+                image_regex!!.toRegex().find(body)!!.groups[1]!!.value
+            else if (image_css != null)
+                Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
+            else
+                throw Exception("image_regex and image_css are null")
+        }
+    }
+}

+ 233 - 233
app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSourceMappings.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/YamlOnlineSourceMappings.kt

@@ -1,234 +1,234 @@
-@file:Suppress("UNCHECKED_CAST")
-
-package eu.kanade.tachiyomi.data.source.online
-
-import eu.kanade.tachiyomi.data.source.model.SManga
-import okhttp3.FormBody
-import okhttp3.RequestBody
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import java.text.ParseException
-import java.text.SimpleDateFormat
-import java.util.*
-
-private fun toMap(map: Any?) = map as? Map<String, Any?>
-
-class YamlSourceNode(uncheckedMap: Map<*, *>) {
-
-    val map = toMap(uncheckedMap)!!
-
-    val id: Any by map
-
-    val name: String by map
-
-    val host: String by map
-
-    val lang: String by map
-
-    val client: String?
-        get() = map["client"] as? String
-
-    val popular = PopularNode(toMap(map["popular"])!!)
-
-    val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
-
-    val search = SearchNode(toMap(map["search"])!!)
-
-    val manga = MangaNode(toMap(map["manga"])!!)
-
-    val chapters = ChaptersNode(toMap(map["chapters"])!!)
-
-    val pages = PagesNode(toMap(map["pages"])!!)
-}
-
-interface RequestableNode {
-
-    val map: Map<String, Any?>
-
-    val url: String
-        get() = map["url"] as String
-
-    val method: String?
-        get() = map["method"] as? String
-
-    val payload: Map<String, String>?
-        get() = map["payload"] as? Map<String, String>
-
-    fun createForm(): RequestBody {
-        return FormBody.Builder().apply {
-            payload?.let {
-                for ((key, value) in it) {
-                    add(key, value)
-                }
-            }
-        }.build()
-    }
-
-}
-
-class PopularNode(override val map: Map<String, Any?>): RequestableNode {
-
-    val manga_css: String by map
-
-    val next_url_css: String?
-        get() = map["next_url_css"] as? String
-
-}
-
-
-class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
-
-    val manga_css: String by map
-
-    val next_url_css: String?
-        get() = map["next_url_css"] as? String
-
-}
-
-
-class SearchNode(override val map: Map<String, Any?>): RequestableNode {
-
-    val manga_css: String by map
-
-    val next_url_css: String?
-        get() = map["next_url_css"] as? String
-}
-
-class MangaNode(private val map: Map<String, Any?>) {
-
-    val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
-
-    val artist = toMap(map["artist"])?.let { SelectableNode(it) }
-
-    val author = toMap(map["author"])?.let { SelectableNode(it) }
-
-    val summary = toMap(map["summary"])?.let { SelectableNode(it) }
-
-    val status = toMap(map["status"])?.let { StatusNode(it) }
-
-    val genres = toMap(map["genres"])?.let { SelectableNode(it) }
-
-    val cover = toMap(map["cover"])?.let { CoverNode(it) }
-
-}
-
-class ChaptersNode(private val map: Map<String, Any?>) {
-
-    val chapter_css: String by map
-
-    val title: String by map
-
-    val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
-}
-
-class CacheNode(private val map: Map<String, Any?>) {
-
-    fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
-}
-
-open class SelectableNode(private val map: Map<String, Any?>) {
-
-    val select: String by map
-
-    val from: String?
-        get() = map["from"] as? String
-
-    open val attr: String?
-        get() = map["attr"] as? String
-
-    val capture: String?
-        get() = map["capture"] as? String
-
-    fun process(document: Element, cache: Map<String, Element>): String {
-        val parent = from?.let { cache[it] } ?: document
-        val node = parent.select(select).first()
-        var text = attr?.let { node.attr(it) } ?: node.text()
-        capture?.let {
-            text = Regex(it).find(text)?.groupValues?.get(1) ?: text
-        }
-        return text
-    }
-}
-
-class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
-
-    val complete: String?
-        get() = map["complete"] as? String
-
-    val ongoing: String?
-        get() = map["ongoing"] as? String
-
-    val licensed: String?
-        get() = map["licensed"] as? String
-
-    fun getStatus(document: Element, cache: Map<String, Element>): Int {
-        val text = process(document, cache)
-        complete?.let {
-            if (text.contains(it)) return SManga.COMPLETED
-        }
-        ongoing?.let {
-            if (text.contains(it)) return SManga.ONGOING
-        }
-        licensed?.let {
-            if (text.contains(it)) return SManga.LICENSED
-        }
-        return SManga.UNKNOWN
-    }
-}
-
-class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
-
-    override val attr: String?
-        get() = map["attr"] as? String ?: "src"
-}
-
-class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
-
-    val format: String by map
-
-    fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
-        val text = process(document, cache)
-        try {
-            return formatter.parse(text)
-        } catch (exception: ParseException) {}
-
-        for (i in 0..7) {
-            (map["day$i"] as? List<String>)?.let {
-                it.find { it.toRegex().containsMatchIn(text) }?.let {
-                    return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
-                }
-            }
-        }
-
-        return Date(0)
-    }
-
-}
-
-class PagesNode(private val map: Map<String, Any?>) {
-
-    val pages_regex: String?
-        get() = map["pages_regex"] as? String
-
-    val pages_css: String?
-        get() = map["pages_css"] as? String
-
-    val pages_attr: String?
-        get() = map["pages_attr"] as? String ?: "value"
-
-    val replace: String?
-        get() = map["url_replace"] as? String
-
-    val replacement: String?
-        get() = map["url_replacement"] as? String
-
-    val image_regex: String?
-        get() = map["image_regex"] as? String
-
-    val image_css: String?
-        get() = map["image_css"] as? String
-
-    val image_attr: String
-        get() = map["image_attr"] as? String ?: "src"
-
+@file:Suppress("UNCHECKED_CAST")
+
+package eu.kanade.tachiyomi.source.online
+
+import eu.kanade.tachiyomi.source.model.SManga
+import okhttp3.FormBody
+import okhttp3.RequestBody
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.*
+
+private fun toMap(map: Any?) = map as? Map<String, Any?>
+
+class YamlSourceNode(uncheckedMap: Map<*, *>) {
+
+    val map = toMap(uncheckedMap)!!
+
+    val id: Any by map
+
+    val name: String by map
+
+    val host: String by map
+
+    val lang: String by map
+
+    val client: String?
+        get() = map["client"] as? String
+
+    val popular = PopularNode(toMap(map["popular"])!!)
+
+    val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
+
+    val search = SearchNode(toMap(map["search"])!!)
+
+    val manga = MangaNode(toMap(map["manga"])!!)
+
+    val chapters = ChaptersNode(toMap(map["chapters"])!!)
+
+    val pages = PagesNode(toMap(map["pages"])!!)
+}
+
+interface RequestableNode {
+
+    val map: Map<String, Any?>
+
+    val url: String
+        get() = map["url"] as String
+
+    val method: String?
+        get() = map["method"] as? String
+
+    val payload: Map<String, String>?
+        get() = map["payload"] as? Map<String, String>
+
+    fun createForm(): RequestBody {
+        return FormBody.Builder().apply {
+            payload?.let {
+                for ((key, value) in it) {
+                    add(key, value)
+                }
+            }
+        }.build()
+    }
+
+}
+
+class PopularNode(override val map: Map<String, Any?>): RequestableNode {
+
+    val manga_css: String by map
+
+    val next_url_css: String?
+        get() = map["next_url_css"] as? String
+
+}
+
+
+class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
+
+    val manga_css: String by map
+
+    val next_url_css: String?
+        get() = map["next_url_css"] as? String
+
+}
+
+
+class SearchNode(override val map: Map<String, Any?>): RequestableNode {
+
+    val manga_css: String by map
+
+    val next_url_css: String?
+        get() = map["next_url_css"] as? String
+}
+
+class MangaNode(private val map: Map<String, Any?>) {
+
+    val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
+
+    val artist = toMap(map["artist"])?.let { SelectableNode(it) }
+
+    val author = toMap(map["author"])?.let { SelectableNode(it) }
+
+    val summary = toMap(map["summary"])?.let { SelectableNode(it) }
+
+    val status = toMap(map["status"])?.let { StatusNode(it) }
+
+    val genres = toMap(map["genres"])?.let { SelectableNode(it) }
+
+    val cover = toMap(map["cover"])?.let { CoverNode(it) }
+
+}
+
+class ChaptersNode(private val map: Map<String, Any?>) {
+
+    val chapter_css: String by map
+
+    val title: String by map
+
+    val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
+}
+
+class CacheNode(private val map: Map<String, Any?>) {
+
+    fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
+}
+
+open class SelectableNode(private val map: Map<String, Any?>) {
+
+    val select: String by map
+
+    val from: String?
+        get() = map["from"] as? String
+
+    open val attr: String?
+        get() = map["attr"] as? String
+
+    val capture: String?
+        get() = map["capture"] as? String
+
+    fun process(document: Element, cache: Map<String, Element>): String {
+        val parent = from?.let { cache[it] } ?: document
+        val node = parent.select(select).first()
+        var text = attr?.let { node.attr(it) } ?: node.text()
+        capture?.let {
+            text = Regex(it).find(text)?.groupValues?.get(1) ?: text
+        }
+        return text
+    }
+}
+
+class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
+
+    val complete: String?
+        get() = map["complete"] as? String
+
+    val ongoing: String?
+        get() = map["ongoing"] as? String
+
+    val licensed: String?
+        get() = map["licensed"] as? String
+
+    fun getStatus(document: Element, cache: Map<String, Element>): Int {
+        val text = process(document, cache)
+        complete?.let {
+            if (text.contains(it)) return SManga.COMPLETED
+        }
+        ongoing?.let {
+            if (text.contains(it)) return SManga.ONGOING
+        }
+        licensed?.let {
+            if (text.contains(it)) return SManga.LICENSED
+        }
+        return SManga.UNKNOWN
+    }
+}
+
+class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
+
+    override val attr: String?
+        get() = map["attr"] as? String ?: "src"
+}
+
+class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
+
+    val format: String by map
+
+    fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
+        val text = process(document, cache)
+        try {
+            return formatter.parse(text)
+        } catch (exception: ParseException) {}
+
+        for (i in 0..7) {
+            (map["day$i"] as? List<String>)?.let {
+                it.find { it.toRegex().containsMatchIn(text) }?.let {
+                    return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
+                }
+            }
+        }
+
+        return Date(0)
+    }
+
+}
+
+class PagesNode(private val map: Map<String, Any?>) {
+
+    val pages_regex: String?
+        get() = map["pages_regex"] as? String
+
+    val pages_css: String?
+        get() = map["pages_css"] as? String
+
+    val pages_attr: String?
+        get() = map["pages_attr"] as? String ?: "value"
+
+    val replace: String?
+        get() = map["url_replace"] as? String
+
+    val replacement: String?
+        get() = map["url_replacement"] as? String
+
+    val image_regex: String?
+        get() = map["image_regex"] as? String
+
+    val image_css: String?
+        get() = map["image_css"] as? String
+
+    val image_attr: String
+        get() = map["image_attr"] as? String ?: "src"
+
 }

+ 365 - 365
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt

@@ -1,366 +1,366 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-import android.text.Html
-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.*
-import eu.kanade.tachiyomi.data.source.online.LoginSource
-import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import eu.kanade.tachiyomi.util.asJsoup
-import eu.kanade.tachiyomi.util.selectText
-import okhttp3.FormBody
-import okhttp3.HttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import rx.Observable
-import java.net.URI
-import java.text.ParseException
-import java.text.SimpleDateFormat
-import java.util.*
-import java.util.regex.Pattern
-
-class Batoto : ParsedOnlineSource(), LoginSource {
-
-    override val id: Long = 1
-
-    override val name = "Batoto"
-
-    override val baseUrl = "http://bato.to"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
-
-    private val dateFields = HashMap<String, Int>().apply {
-        put("second", Calendar.SECOND)
-        put("minute", Calendar.MINUTE)
-        put("hour", Calendar.HOUR)
-        put("day", Calendar.DATE)
-        put("week", Calendar.WEEK_OF_YEAR)
-        put("month", Calendar.MONTH)
-        put("year", Calendar.YEAR)
-    }
-
-    private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
-
-    override fun headersBuilder() = super.headersBuilder()
-            .add("Cookie", "lang_option=English")
-
-    private val pageHeaders = super.headersBuilder()
-            .add("Referer", "http://bato.to/reader")
-            .build()
-
-    override fun popularMangaRequest(page: Int): Request {
-        return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
-    }
-
-    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): 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun popularMangaNextPageSelector() = "#show_more_row"
-
-    override fun latestUpdatesNextPageSelector() = "#show_more_row"
-
-    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 = ""
-        filters.forEach { filter ->
-            when (filter) {
-                is Status -> if (!filter.isIgnored()) {
-                    url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
-                }
-                is GenreList -> {
-                    filter.state.forEach { filter ->
-                        when (filter) {
-                            is Genre -> if (!filter.isIgnored()) {
-                                genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
-                            }
-                            is SelectField -> {
-                                val sel = filter.values[filter.state].value
-                                if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
-                            }
-                        }
-                    }
-                }
-                is TextField -> {
-                    if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
-                }
-                is SelectField -> {
-                    val sel = filter.values[filter.state].value
-                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
-                }
-                is Flag -> {
-                    val sel = if (filter.state) filter.valTrue else filter.valFalse
-                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
-                }
-                is OrderBy -> {
-                    url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
-                    url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
-                }
-            }
-        }
-        if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
-        url.addQueryParameter("p", page.toString())
-        return GET(url.toString(), headers)
-    }
-
-    override fun searchMangaSelector() = popularMangaSelector()
-
-    override fun searchMangaFromElement(element: Element): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
-
-    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): 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" -> SManga.ONGOING
-        "Complete" -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    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)
-        return document.select(chapterListSelector()).map { chapterFromElement(it) }
-    }
-
-    override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
-
-    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 {
-        val dateAsString = dateElement.text()
-
-        var date: Date
-        try {
-            date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
-        } catch (e: ParseException) {
-            val m = datePattern.matcher(dateAsString)
-
-            if (m.matches()) {
-                val number = m.group(1)
-                val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
-                val unit = m.group(2)
-
-                date = Calendar.getInstance().apply {
-                    add(dateFields[unit]!!, -amount)
-                }.time
-            } else {
-                return 0
-            }
-        }
-
-        return date.time
-    }
-
-    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): List<Page> {
-        val pages = mutableListOf<Page>()
-        val selectElement = document.select("#page_select").first()
-        if (selectElement != null) {
-            for ((i, element) in selectElement.select("option").withIndex()) {
-                pages.add(Page(i, element.attr("value")))
-            }
-            pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
-        } else {
-            // For webtoons in one page
-            for ((i, element) in document.select("div > img").withIndex()) {
-                pages.add(Page(i, "", element.attr("src")))
-            }
-        }
-        return pages
-    }
-
-    override fun imageUrlRequest(page: Page): Request {
-        val pageUrl = page.url
-        val start = pageUrl.indexOf("#") + 1
-        val end = pageUrl.indexOf("_", start)
-        val id = pageUrl.substring(start, end)
-        return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
-    }
-
-    override fun imageUrlParse(document: Document): String {
-        return document.select("#comic_page").first().attr("src")
-    }
-
-    override fun login(username: String, password: String) =
-            client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
-                    .asObservable()
-                    .flatMap { doLogin(it, username, password) }
-                    .map { isAuthenticationSuccessful(it) }
-
-    private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
-        val doc = response.asJsoup()
-        val form = doc.select("#login").first()
-        val url = form.attr("action")
-        val authKey = form.select("input[name=auth_key]").first()
-
-        val payload = FormBody.Builder().apply {
-            add(authKey.attr("name"), authKey.attr("value"))
-            add("ips_username", username)
-            add("ips_password", password)
-            add("invisible", "1")
-            add("rememberMe", "1")
-        }.build()
-
-        return client.newCall(POST(url, headers, payload)).asObservable()
-    }
-
-    override fun isAuthenticationSuccessful(response: Response) =
-            response.priorResponse() != null && response.priorResponse().code() == 302
-
-    override fun isLogged(): Boolean {
-        return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
-    }
-
-    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
-        if (!isLogged()) {
-            val username = preferences.sourceUsername(this)
-            val password = preferences.sourcePassword(this)
-
-            if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
-                return Observable.error(Exception("User not logged"))
-            } else {
-                return login(username, password).flatMap { super.fetchChapterList(manga) }
-            }
-
-        } else {
-            return super.fetchChapterList(manga)
-        }
-    }
-
-    private data class ListValue(val name: String, val value: String) {
-        override fun toString(): String = name
-    }
-
-    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 SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
-    private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
-    private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
-    private class OrderBy : Filter.Sort("Order by",
-            arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
-            Filter.Sort.Selection(4, false))
-
-    override fun getFilterList() = FilterList(
-            TextField("Author", "artist_name"),
-            SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
-            Status(),
-            Flag("Exclude mature", "mature", "m", ""),
-            OrderBy(),
-            GenreList(getGenreList())
-    )
-
-    // [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
-    //     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
-    private fun getGenreList() = listOf(
-            SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
-            Genre("4-Koma", 40),
-            Genre("Action", 1),
-            Genre("Adventure", 2),
-            Genre("Award Winning", 39),
-            Genre("Comedy", 3),
-            Genre("Cooking", 41),
-            Genre("Doujinshi", 9),
-            Genre("Drama", 10),
-            Genre("Ecchi", 12),
-            Genre("Fantasy", 13),
-            Genre("Gender Bender", 15),
-            Genre("Harem", 17),
-            Genre("Historical", 20),
-            Genre("Horror", 22),
-            Genre("Josei", 34),
-            Genre("Martial Arts", 27),
-            Genre("Mecha", 30),
-            Genre("Medical", 42),
-            Genre("Music", 37),
-            Genre("Mystery", 4),
-            Genre("Oneshot", 38),
-            Genre("Psychological", 5),
-            Genre("Romance", 6),
-            Genre("School Life", 7),
-            Genre("Sci-fi", 8),
-            Genre("Seinen", 32),
-            Genre("Shoujo", 35),
-            Genre("Shoujo Ai", 16),
-            Genre("Shounen", 33),
-            Genre("Shounen Ai", 19),
-            Genre("Slice of Life", 21),
-            Genre("Smut", 23),
-            Genre("Sports", 25),
-            Genre("Supernatural", 26),
-            Genre("Tragedy", 28),
-            Genre("Webtoon", 36),
-            Genre("Yaoi", 29),
-            Genre("Yuri", 31),
-            Genre("[no chapters]", 44)
-    )
-
+package eu.kanade.tachiyomi.source.online.english
+
+import android.text.Html
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservable
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import eu.kanade.tachiyomi.util.asJsoup
+import eu.kanade.tachiyomi.util.selectText
+import okhttp3.FormBody
+import okhttp3.HttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import java.net.URI
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.regex.Pattern
+
+class Batoto : ParsedOnlineSource(), LoginSource {
+
+    override val id: Long = 1
+
+    override val name = "Batoto"
+
+    override val baseUrl = "http://bato.to"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
+
+    private val dateFields = HashMap<String, Int>().apply {
+        put("second", Calendar.SECOND)
+        put("minute", Calendar.MINUTE)
+        put("hour", Calendar.HOUR)
+        put("day", Calendar.DATE)
+        put("week", Calendar.WEEK_OF_YEAR)
+        put("month", Calendar.MONTH)
+        put("year", Calendar.YEAR)
+    }
+
+    private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
+
+    override fun headersBuilder() = super.headersBuilder()
+            .add("Cookie", "lang_option=English")
+
+    private val pageHeaders = super.headersBuilder()
+            .add("Referer", "http://bato.to/reader")
+            .build()
+
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
+    }
+
+    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): 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun popularMangaNextPageSelector() = "#show_more_row"
+
+    override fun latestUpdatesNextPageSelector() = "#show_more_row"
+
+    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 = ""
+        filters.forEach { filter ->
+            when (filter) {
+                is Status -> if (!filter.isIgnored()) {
+                    url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
+                }
+                is GenreList -> {
+                    filter.state.forEach { filter ->
+                        when (filter) {
+                            is Genre -> if (!filter.isIgnored()) {
+                                genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
+                            }
+                            is SelectField -> {
+                                val sel = filter.values[filter.state].value
+                                if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
+                            }
+                        }
+                    }
+                }
+                is TextField -> {
+                    if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
+                }
+                is SelectField -> {
+                    val sel = filter.values[filter.state].value
+                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
+                }
+                is Flag -> {
+                    val sel = if (filter.state) filter.valTrue else filter.valFalse
+                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
+                }
+                is OrderBy -> {
+                    url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
+                    url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
+                }
+            }
+        }
+        if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
+        url.addQueryParameter("p", page.toString())
+        return GET(url.toString(), headers)
+    }
+
+    override fun searchMangaSelector() = popularMangaSelector()
+
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+    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): 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" -> SManga.ONGOING
+        "Complete" -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    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)
+        return document.select(chapterListSelector()).map { chapterFromElement(it) }
+    }
+
+    override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
+
+    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 {
+        val dateAsString = dateElement.text()
+
+        var date: Date
+        try {
+            date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
+        } catch (e: ParseException) {
+            val m = datePattern.matcher(dateAsString)
+
+            if (m.matches()) {
+                val number = m.group(1)
+                val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
+                val unit = m.group(2)
+
+                date = Calendar.getInstance().apply {
+                    add(dateFields[unit]!!, -amount)
+                }.time
+            } else {
+                return 0
+            }
+        }
+
+        return date.time
+    }
+
+    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): List<Page> {
+        val pages = mutableListOf<Page>()
+        val selectElement = document.select("#page_select").first()
+        if (selectElement != null) {
+            for ((i, element) in selectElement.select("option").withIndex()) {
+                pages.add(Page(i, element.attr("value")))
+            }
+            pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        } else {
+            // For webtoons in one page
+            for ((i, element) in document.select("div > img").withIndex()) {
+                pages.add(Page(i, "", element.attr("src")))
+            }
+        }
+        return pages
+    }
+
+    override fun imageUrlRequest(page: Page): Request {
+        val pageUrl = page.url
+        val start = pageUrl.indexOf("#") + 1
+        val end = pageUrl.indexOf("_", start)
+        val id = pageUrl.substring(start, end)
+        return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
+    }
+
+    override fun imageUrlParse(document: Document): String {
+        return document.select("#comic_page").first().attr("src")
+    }
+
+    override fun login(username: String, password: String) =
+            client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
+                    .asObservable()
+                    .flatMap { doLogin(it, username, password) }
+                    .map { isAuthenticationSuccessful(it) }
+
+    private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
+        val doc = response.asJsoup()
+        val form = doc.select("#login").first()
+        val url = form.attr("action")
+        val authKey = form.select("input[name=auth_key]").first()
+
+        val payload = FormBody.Builder().apply {
+            add(authKey.attr("name"), authKey.attr("value"))
+            add("ips_username", username)
+            add("ips_password", password)
+            add("invisible", "1")
+            add("rememberMe", "1")
+        }.build()
+
+        return client.newCall(POST(url, headers, payload)).asObservable()
+    }
+
+    override fun isAuthenticationSuccessful(response: Response) =
+            response.priorResponse() != null && response.priorResponse().code() == 302
+
+    override fun isLogged(): Boolean {
+        return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
+    }
+
+    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
+        if (!isLogged()) {
+            val username = preferences.sourceUsername(this)
+            val password = preferences.sourcePassword(this)
+
+            if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
+                return Observable.error(Exception("User not logged"))
+            } else {
+                return login(username, password).flatMap { super.fetchChapterList(manga) }
+            }
+
+        } else {
+            return super.fetchChapterList(manga)
+        }
+    }
+
+    private data class ListValue(val name: String, val value: String) {
+        override fun toString(): String = name
+    }
+
+    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 SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
+    private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
+    private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
+    private class OrderBy : Filter.Sort("Order by",
+            arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
+            Filter.Sort.Selection(4, false))
+
+    override fun getFilterList() = FilterList(
+            TextField("Author", "artist_name"),
+            SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
+            Status(),
+            Flag("Exclude mature", "mature", "m", ""),
+            OrderBy(),
+            GenreList(getGenreList())
+    )
+
+    // [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
+    //     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
+    private fun getGenreList() = listOf(
+            SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
+            Genre("4-Koma", 40),
+            Genre("Action", 1),
+            Genre("Adventure", 2),
+            Genre("Award Winning", 39),
+            Genre("Comedy", 3),
+            Genre("Cooking", 41),
+            Genre("Doujinshi", 9),
+            Genre("Drama", 10),
+            Genre("Ecchi", 12),
+            Genre("Fantasy", 13),
+            Genre("Gender Bender", 15),
+            Genre("Harem", 17),
+            Genre("Historical", 20),
+            Genre("Horror", 22),
+            Genre("Josei", 34),
+            Genre("Martial Arts", 27),
+            Genre("Mecha", 30),
+            Genre("Medical", 42),
+            Genre("Music", 37),
+            Genre("Mystery", 4),
+            Genre("Oneshot", 38),
+            Genre("Psychological", 5),
+            Genre("Romance", 6),
+            Genre("School Life", 7),
+            Genre("Sci-fi", 8),
+            Genre("Seinen", 32),
+            Genre("Shoujo", 35),
+            Genre("Shoujo Ai", 16),
+            Genre("Shounen", 33),
+            Genre("Shounen Ai", 19),
+            Genre("Slice of Life", 21),
+            Genre("Smut", 23),
+            Genre("Sports", 25),
+            Genre("Supernatural", 26),
+            Genre("Tragedy", 28),
+            Genre("Webtoon", 36),
+            Genre("Yaoi", 29),
+            Genre("Yuri", 31),
+            Genre("[no chapters]", 44)
+    )
+
 }

+ 196 - 196
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Kissmanga.kt

@@ -1,197 +1,197 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-import eu.kanade.tachiyomi.data.network.GET
-import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.*
-import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import okhttp3.FormBody
-import okhttp3.OkHttpClient
-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 Kissmanga : ParsedOnlineSource() {
-
-    override val id: Long = 4
-
-    override val name = "Kissmanga"
-
-    override val baseUrl = "http://kissmanga.com"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    override val client: OkHttpClient = network.cloudflareClient
-
-    override fun popularMangaSelector() = "table.listing tr:gt(1)"
-
-    override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
-
-    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): 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: Int, query: String, filters: FilterList): Request {
-        val form = FormBody.Builder().apply {
-            add("mangaName", query)
-
-            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])
-                    is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
-                }
-            }
-        }
-        return POST("$baseUrl/AdvanceSearch", headers, form.build())
-    }
-
-    override fun searchMangaSelector() = popularMangaSelector()
-
-    override fun searchMangaFromElement(element: Element): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun searchMangaNextPageSelector() = null
-
-    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") -> SManga.ONGOING
-        status.contains("Completed") -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    override fun chapterListSelector() = "table.listing tr:gt(1)"
-
-    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: SChapter) = POST(baseUrl + chapter.url, headers)
-
-    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())
-
-        var i = 0
-        while (m.find()) {
-            pages.add(Page(i++, "", m.group(1)))
-        }
-        return pages
-    }
-
-    override fun pageListParse(document: Document): List<Page> {
-        throw Exception("Not used")
-    }
-
-    override fun imageUrlRequest(page: Page) = GET(page.url)
-
-    override fun imageUrlParse(document: Document) = ""
-
-    private class Status : Filter.TriState("Completed")
-    private class Author : Filter.Text("Author")
-    private class Genre(name: String) : Filter.TriState(name)
-    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
-
-    override fun getFilterList() = FilterList(
-            Author(),
-            Status(),
-            GenreList(getGenreList())
-    )
-
-    // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
-    // on http://kissmanga.com/AdvanceSearch
-    private fun getGenreList() = listOf(
-            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")
-    )
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+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 Kissmanga : ParsedOnlineSource() {
+
+    override val id: Long = 4
+
+    override val name = "Kissmanga"
+
+    override val baseUrl = "http://kissmanga.com"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    override val client: OkHttpClient = network.cloudflareClient
+
+    override fun popularMangaSelector() = "table.listing tr:gt(1)"
+
+    override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
+
+    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): 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: Int, query: String, filters: FilterList): Request {
+        val form = FormBody.Builder().apply {
+            add("mangaName", query)
+
+            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])
+                    is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
+                }
+            }
+        }
+        return POST("$baseUrl/AdvanceSearch", headers, form.build())
+    }
+
+    override fun searchMangaSelector() = popularMangaSelector()
+
+    override fun searchMangaFromElement(element: Element): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun searchMangaNextPageSelector() = null
+
+    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") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    override fun chapterListSelector() = "table.listing tr:gt(1)"
+
+    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: SChapter) = POST(baseUrl + chapter.url, headers)
+
+    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())
+
+        var i = 0
+        while (m.find()) {
+            pages.add(Page(i++, "", m.group(1)))
+        }
+        return pages
+    }
+
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
+    }
+
+    override fun imageUrlRequest(page: Page) = GET(page.url)
+
+    override fun imageUrlParse(document: Document) = ""
+
+    private class Status : Filter.TriState("Completed")
+    private class Author : Filter.Text("Author")
+    private class Genre(name: String) : Filter.TriState(name)
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            Author(),
+            Status(),
+            GenreList(getGenreList())
+    )
+
+    // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
+    // on http://kissmanga.com/AdvanceSearch
+    private fun getGenreList() = listOf(
+            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")
+    )
 }

+ 222 - 222
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangafox.kt

@@ -1,223 +1,223 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-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 Mangafox : ParsedOnlineSource() {
-
-    override val id: Long = 3
-
-    override val name = "Mangafox"
-
-    override val baseUrl = "http://mangafox.me"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    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 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun popularMangaNextPageSelector() = "a:has(span.next)"
-
-    override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
-
-    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)
-        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
-            when (filter) {
-                is Status -> url.addQueryParameter(filter.id, filter.state.toString())
-                is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
-                is TextField -> url.addQueryParameter(filter.key, filter.state)
-                is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
-                is OrderBy -> {
-                    url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
-                    url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
-                }
-            }
-        }
-        url.addQueryParameter("page", page.toString())
-        return GET(url.toString(), headers)
-    }
-
-    override fun searchMangaSelector() = "div#mangalist > ul.list > li"
-
-    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): 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") -> SManga.ONGOING
-        status.contains("Completed") -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    override fun chapterListSelector() = "div#chapters li div"
-
-    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 {
-        return if ("Today" in date || " ago" in date) {
-            Calendar.getInstance().apply {
-                set(Calendar.HOUR_OF_DAY, 0)
-                set(Calendar.MINUTE, 0)
-                set(Calendar.SECOND, 0)
-                set(Calendar.MILLISECOND, 0)
-            }.timeInMillis
-        } else if ("Yesterday" in date) {
-            Calendar.getInstance().apply {
-                add(Calendar.DATE, -1)
-                set(Calendar.HOUR_OF_DAY, 0)
-                set(Calendar.MINUTE, 0)
-                set(Calendar.SECOND, 0)
-                set(Calendar.MILLISECOND, 0)
-            }.timeInMillis
-        } else {
-            try {
-                SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
-            } catch (e: ParseException) {
-                0L
-            }
-        }
-    }
-
-    override fun pageListParse(document: Document): List<Page> {
-        val url = document.baseUri().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"))
-        }
-        return pages
-    }
-
-    override fun imageUrlParse(document: Document): String {
-        val url = document.getElementById("image").attr("src")
-        return if ("compressed?token=" !in url) {
-            url
-        } else {
-            "http://mangafox.me/media/logo.png"
-        }
-    }
-
-    private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
-    private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
-    private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
-    private class OrderBy : Filter.Sort("Order by",
-            arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
-            Filter.Sort.Selection(2, false))
-    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
-
-    override fun getFilterList() = FilterList(
-            TextField("Author", "author"),
-            TextField("Artist", "artist"),
-            Type(),
-            Status(),
-            OrderBy(),
-            GenreList(getGenreList())
-    )
-
-    // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
-    // on http://mangafox.me/search.php
-    private fun getGenreList() = listOf(
-            Genre("Action"),
-            Genre("Adult"),
-            Genre("Adventure"),
-            Genre("Comedy"),
-            Genre("Doujinshi"),
-            Genre("Drama"),
-            Genre("Ecchi"),
-            Genre("Fantasy"),
-            Genre("Gender Bender"),
-            Genre("Harem"),
-            Genre("Historical"),
-            Genre("Horror"),
-            Genre("Josei"),
-            Genre("Martial Arts"),
-            Genre("Mature"),
-            Genre("Mecha"),
-            Genre("Mystery"),
-            Genre("One Shot"),
-            Genre("Psychological"),
-            Genre("Romance"),
-            Genre("School Life"),
-            Genre("Sci-fi"),
-            Genre("Seinen"),
-            Genre("Shoujo"),
-            Genre("Shoujo Ai"),
-            Genre("Shounen"),
-            Genre("Shounen Ai"),
-            Genre("Slice of Life"),
-            Genre("Smut"),
-            Genre("Sports"),
-            Genre("Supernatural"),
-            Genre("Tragedy"),
-            Genre("Webtoons"),
-            Genre("Yaoi"),
-            Genre("Yuri")
-    )
-
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.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 Mangafox : ParsedOnlineSource() {
+
+    override val id: Long = 3
+
+    override val name = "Mangafox"
+
+    override val baseUrl = "http://mangafox.me"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    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 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun popularMangaNextPageSelector() = "a:has(span.next)"
+
+    override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
+
+    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)
+        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
+            when (filter) {
+                is Status -> url.addQueryParameter(filter.id, filter.state.toString())
+                is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
+                is TextField -> url.addQueryParameter(filter.key, filter.state)
+                is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
+                is OrderBy -> {
+                    url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
+                    url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
+                }
+            }
+        }
+        url.addQueryParameter("page", page.toString())
+        return GET(url.toString(), headers)
+    }
+
+    override fun searchMangaSelector() = "div#mangalist > ul.list > li"
+
+    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): 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") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    override fun chapterListSelector() = "div#chapters li div"
+
+    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 {
+        return if ("Today" in date || " ago" in date) {
+            Calendar.getInstance().apply {
+                set(Calendar.HOUR_OF_DAY, 0)
+                set(Calendar.MINUTE, 0)
+                set(Calendar.SECOND, 0)
+                set(Calendar.MILLISECOND, 0)
+            }.timeInMillis
+        } else if ("Yesterday" in date) {
+            Calendar.getInstance().apply {
+                add(Calendar.DATE, -1)
+                set(Calendar.HOUR_OF_DAY, 0)
+                set(Calendar.MINUTE, 0)
+                set(Calendar.SECOND, 0)
+                set(Calendar.MILLISECOND, 0)
+            }.timeInMillis
+        } else {
+            try {
+                SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
+            } catch (e: ParseException) {
+                0L
+            }
+        }
+    }
+
+    override fun pageListParse(document: Document): List<Page> {
+        val url = document.baseUri().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"))
+        }
+        return pages
+    }
+
+    override fun imageUrlParse(document: Document): String {
+        val url = document.getElementById("image").attr("src")
+        return if ("compressed?token=" !in url) {
+            url
+        } else {
+            "http://mangafox.me/media/logo.png"
+        }
+    }
+
+    private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
+    private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
+    private class TextField(name: String, val key: String) : Filter.Text(name)
+    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
+    private class OrderBy : Filter.Sort("Order by",
+            arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
+            Filter.Sort.Selection(2, false))
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            TextField("Author", "author"),
+            TextField("Artist", "artist"),
+            Type(),
+            Status(),
+            OrderBy(),
+            GenreList(getGenreList())
+    )
+
+    // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
+    // on http://mangafox.me/search.php
+    private fun getGenreList() = listOf(
+            Genre("Action"),
+            Genre("Adult"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            Genre("One Shot"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Smut"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Webtoons"),
+            Genre("Yaoi"),
+            Genre("Yuri")
+    )
+
 }

+ 219 - 219
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangahere.kt

@@ -1,220 +1,220 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-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 : ParsedOnlineSource() {
-
-    override val id: Long = 2
-
-    override val name = "Mangahere"
-
-    override val baseUrl = "http://www.mangahere.co"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    override fun popularMangaSelector() = "div.directory_list > ul > li"
-
-    override fun latestUpdatesSelector() = "div.directory_list > ul > li"
-
-    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): SManga {
-        return mangaFromElement("div.title > a", element)
-    }
-
-    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 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)
-        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
-            when (filter) {
-                is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
-                is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
-                is TextField -> url.addQueryParameter(filter.key, filter.state)
-                is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
-                is OrderBy -> {
-                    url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
-                    url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
-                }
-            }
-        }
-        url.addQueryParameter("page", page.toString())
-        return GET(url.toString(), headers)
-    }
-
-    override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
-
-    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): 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") -> SManga.ONGOING
-        status.contains("Completed") -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
-
-    override fun chapterFromElement(element: Element): SChapter {
-        val parentEl = element.select("span.left").first()
-
-        val urlElement = parentEl.select("a").first()
-
-        var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
-        if (volume.length > 0) {
-            volume = " - " + volume
-        }
-
-        var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
-        if (title.length > 0) {
-            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 {
-        return if ("Today" in date) {
-            Calendar.getInstance().apply {
-                set(Calendar.HOUR_OF_DAY, 0)
-                set(Calendar.MINUTE, 0)
-                set(Calendar.SECOND, 0)
-                set(Calendar.MILLISECOND, 0)
-            }.timeInMillis
-        } else if ("Yesterday" in date) {
-            Calendar.getInstance().apply {
-                add(Calendar.DATE, -1)
-                set(Calendar.HOUR_OF_DAY, 0)
-                set(Calendar.MINUTE, 0)
-                set(Calendar.SECOND, 0)
-                set(Calendar.MILLISECOND, 0)
-            }.timeInMillis
-        } else {
-            try {
-                SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
-            } catch (e: ParseException) {
-                0L
-            }
-        }
-    }
-
-    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")
-
-    private class Status : Filter.TriState("Completed")
-    private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
-    private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
-    private class OrderBy : Filter.Sort("Order by",
-            arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
-            Filter.Sort.Selection(2, false))
-    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
-
-    override fun getFilterList() = FilterList(
-            TextField("Author", "author"),
-            TextField("Artist", "artist"),
-            Type(),
-            Status(),
-            OrderBy(),
-            GenreList(getGenreList())
-    )
-
-    // [...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
-    private fun getGenreList() = listOf(
-            Genre("Action"),
-            Genre("Adventure"),
-            Genre("Comedy"),
-            Genre("Doujinshi"),
-            Genre("Drama"),
-            Genre("Ecchi"),
-            Genre("Fantasy"),
-            Genre("Gender Bender"),
-            Genre("Harem"),
-            Genre("Historical"),
-            Genre("Horror"),
-            Genre("Josei"),
-            Genre("Martial Arts"),
-            Genre("Mature"),
-            Genre("Mecha"),
-            Genre("Mystery"),
-            Genre("One Shot"),
-            Genre("Psychological"),
-            Genre("Romance"),
-            Genre("School Life"),
-            Genre("Sci-fi"),
-            Genre("Seinen"),
-            Genre("Shoujo"),
-            Genre("Shoujo Ai"),
-            Genre("Shounen"),
-            Genre("Shounen Ai"),
-            Genre("Slice of Life"),
-            Genre("Sports"),
-            Genre("Supernatural"),
-            Genre("Tragedy"),
-            Genre("Yaoi"),
-            Genre("Yuri")
-    )
-
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.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 : ParsedOnlineSource() {
+
+    override val id: Long = 2
+
+    override val name = "Mangahere"
+
+    override val baseUrl = "http://www.mangahere.co"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    override fun popularMangaSelector() = "div.directory_list > ul > li"
+
+    override fun latestUpdatesSelector() = "div.directory_list > ul > li"
+
+    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): SManga {
+        return mangaFromElement("div.title > a", element)
+    }
+
+    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 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)
+        (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
+            when (filter) {
+                is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
+                is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
+                is TextField -> url.addQueryParameter(filter.key, filter.state)
+                is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
+                is OrderBy -> {
+                    url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
+                    url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
+                }
+            }
+        }
+        url.addQueryParameter("page", page.toString())
+        return GET(url.toString(), headers)
+    }
+
+    override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
+
+    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): 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") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
+
+    override fun chapterFromElement(element: Element): SChapter {
+        val parentEl = element.select("span.left").first()
+
+        val urlElement = parentEl.select("a").first()
+
+        var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
+        if (volume.length > 0) {
+            volume = " - " + volume
+        }
+
+        var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
+        if (title.length > 0) {
+            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 {
+        return if ("Today" in date) {
+            Calendar.getInstance().apply {
+                set(Calendar.HOUR_OF_DAY, 0)
+                set(Calendar.MINUTE, 0)
+                set(Calendar.SECOND, 0)
+                set(Calendar.MILLISECOND, 0)
+            }.timeInMillis
+        } else if ("Yesterday" in date) {
+            Calendar.getInstance().apply {
+                add(Calendar.DATE, -1)
+                set(Calendar.HOUR_OF_DAY, 0)
+                set(Calendar.MINUTE, 0)
+                set(Calendar.SECOND, 0)
+                set(Calendar.MILLISECOND, 0)
+            }.timeInMillis
+        } else {
+            try {
+                SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
+            } catch (e: ParseException) {
+                0L
+            }
+        }
+    }
+
+    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")
+
+    private class Status : Filter.TriState("Completed")
+    private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
+    private class TextField(name: String, val key: String) : Filter.Text(name)
+    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
+    private class OrderBy : Filter.Sort("Order by",
+            arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
+            Filter.Sort.Selection(2, false))
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            TextField("Author", "author"),
+            TextField("Artist", "artist"),
+            Type(),
+            Status(),
+            OrderBy(),
+            GenreList(getGenreList())
+    )
+
+    // [...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
+    private fun getGenreList() = listOf(
+            Genre("Action"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            Genre("One Shot"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Yaoi"),
+            Genre("Yuri")
+    )
+
 }

+ 242 - 242
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Mangasee.kt

@@ -1,243 +1,243 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.*
-import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import okhttp3.FormBody
-import okhttp3.HttpUrl
-import okhttp3.Request
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import java.text.SimpleDateFormat
-import java.util.regex.Pattern
-
-class Mangasee : ParsedOnlineSource() {
-
-    override val id: Long = 9
-
-    override val name = "Mangasee"
-
-    override val baseUrl = "http://mangaseeonline.net"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
-
-    private val indexPattern = Pattern.compile("-index-(.*?)-")
-
-    override fun popularMangaSelector() = "div.requested > div.row"
-
-    override fun popularMangaRequest(page: Int): Request {
-        val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
-        return POST(requestUrl, headers, body.build())
-    }
-
-    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
-    }
-
-    override fun popularMangaNextPageSelector() = "button.requestMore"
-
-    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)
-        val genres = mutableListOf<String>()
-        val genresNo = mutableListOf<String>()
-        for (filter in if (filters.isEmpty()) getFilterList() else filters) {
-            when (filter) {
-                is Sort -> {
-                    if (filter.state?.index != 0)
-                        url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
-                    if (filter.state?.ascending != true)
-                        url.addQueryParameter("sortOrder", "descending")
-                }
-                is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
-                is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
-                is GenreList -> filter.state.forEach { genre ->
-                    when (genre.state) {
-                        Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
-                        Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
-                    }
-                }
-            }
-        }
-        if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
-        if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
-
-        val (body, requestUrl) = convertQueryToPost(page, url.toString())
-        return POST(requestUrl, headers, body.build())
-    }
-
-    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))
-        }
-        val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
-        return Pair(body, requestUrl)
-    }
-
-    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
-    }
-
-    override fun searchMangaNextPageSelector() = "button.requestMore"
-
-    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)") -> SManga.ONGOING
-        status.contains("Complete (Scan)") -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    override fun chapterListSelector() = "div.chapter-list > a"
-
-    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(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 = ""
-
-        val m = indexPattern.matcher(fullUrl)
-        if (m.find()) {
-            val indexNumber = m.group(1)
-            index = "-index-$indexNumber"
-        }
-
-        document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
-            pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
-        }
-        pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
-        return pages
-    }
-
-    override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
-
-    override fun latestUpdatesNextPageSelector() = "button.requestMore"
-
-    override fun latestUpdatesSelector(): String = "a.latestSeries"
-
-    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 latestUpdatesFromElement(element: Element): SManga {
-        val manga = SManga.create()
-        element.select("a.latestSeries").first().let {
-            val chapterUrl = it.attr("href")
-            val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
-            val indexOfLastPath = chapterUrl.lastIndexOf("/")
-            val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
-            val defaultText = it.select("p.clamp2").text()
-            val m = recentUpdatesPattern.matcher(defaultText)
-            val title = if (m.matches()) m.group(1) else defaultText
-            manga.setUrlWithoutDomain("/manga" + mangaUrl)
-            manga.title = title
-        }
-        return manga
-    }
-
-    private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
-    private class Genre(name: String) : Filter.TriState(name)
-    private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
-    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
-
-    override fun getFilterList() = FilterList(
-            TextField("Years", "year"),
-            TextField("Author", "author"),
-            SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
-            SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
-            SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
-            Sort(),
-            GenreList(getGenreList())
-    )
-
-    // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
-    // http://mangasee.co/advanced-search/
-    private fun getGenreList() = listOf(
-            Genre("Action"),
-            Genre("Adult"),
-            Genre("Adventure"),
-            Genre("Comedy"),
-            Genre("Doujinshi"),
-            Genre("Drama"),
-            Genre("Ecchi"),
-            Genre("Fantasy"),
-            Genre("Gender Bender"),
-            Genre("Harem"),
-            Genre("Hentai"),
-            Genre("Historical"),
-            Genre("Horror"),
-            Genre("Josei"),
-            Genre("Lolicon"),
-            Genre("Martial Arts"),
-            Genre("Mature"),
-            Genre("Mecha"),
-            Genre("Mystery"),
-            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("Yaoi"),
-            Genre("Yuri")
-    )
-
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.FormBody
+import okhttp3.HttpUrl
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.regex.Pattern
+
+class Mangasee : ParsedOnlineSource() {
+
+    override val id: Long = 9
+
+    override val name = "Mangasee"
+
+    override val baseUrl = "http://mangaseeonline.net"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
+
+    private val indexPattern = Pattern.compile("-index-(.*?)-")
+
+    override fun popularMangaSelector() = "div.requested > div.row"
+
+    override fun popularMangaRequest(page: Int): Request {
+        val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
+        return POST(requestUrl, headers, body.build())
+    }
+
+    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
+    }
+
+    override fun popularMangaNextPageSelector() = "button.requestMore"
+
+    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)
+        val genres = mutableListOf<String>()
+        val genresNo = mutableListOf<String>()
+        for (filter in if (filters.isEmpty()) getFilterList() else filters) {
+            when (filter) {
+                is Sort -> {
+                    if (filter.state?.index != 0)
+                        url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
+                    if (filter.state?.ascending != true)
+                        url.addQueryParameter("sortOrder", "descending")
+                }
+                is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
+                is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
+                is GenreList -> filter.state.forEach { genre ->
+                    when (genre.state) {
+                        Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
+                        Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
+                    }
+                }
+            }
+        }
+        if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
+        if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
+
+        val (body, requestUrl) = convertQueryToPost(page, url.toString())
+        return POST(requestUrl, headers, body.build())
+    }
+
+    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))
+        }
+        val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
+        return Pair(body, requestUrl)
+    }
+
+    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
+    }
+
+    override fun searchMangaNextPageSelector() = "button.requestMore"
+
+    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)") -> SManga.ONGOING
+        status.contains("Complete (Scan)") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    override fun chapterListSelector() = "div.chapter-list > a"
+
+    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(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 = ""
+
+        val m = indexPattern.matcher(fullUrl)
+        if (m.find()) {
+            val indexNumber = m.group(1)
+            index = "-index-$indexNumber"
+        }
+
+        document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
+            pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
+        }
+        pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
+        return pages
+    }
+
+    override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
+
+    override fun latestUpdatesNextPageSelector() = "button.requestMore"
+
+    override fun latestUpdatesSelector(): String = "a.latestSeries"
+
+    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 latestUpdatesFromElement(element: Element): SManga {
+        val manga = SManga.create()
+        element.select("a.latestSeries").first().let {
+            val chapterUrl = it.attr("href")
+            val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
+            val indexOfLastPath = chapterUrl.lastIndexOf("/")
+            val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
+            val defaultText = it.select("p.clamp2").text()
+            val m = recentUpdatesPattern.matcher(defaultText)
+            val title = if (m.matches()) m.group(1) else defaultText
+            manga.setUrlWithoutDomain("/manga" + mangaUrl)
+            manga.title = title
+        }
+        return manga
+    }
+
+    private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
+    private class Genre(name: String) : Filter.TriState(name)
+    private class TextField(name: String, val key: String) : Filter.Text(name)
+    private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            TextField("Years", "year"),
+            TextField("Author", "author"),
+            SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
+            SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
+            SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
+            Sort(),
+            GenreList(getGenreList())
+    )
+
+    // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
+    // http://mangasee.co/advanced-search/
+    private fun getGenreList() = listOf(
+            Genre("Action"),
+            Genre("Adult"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Hentai"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Lolicon"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            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("Yaoi"),
+            Genre("Yuri")
+    )
+
 }

+ 218 - 218
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/english/Readmangatoday.kt

@@ -1,219 +1,219 @@
-package eu.kanade.tachiyomi.data.source.online.english
-
-import eu.kanade.tachiyomi.data.network.GET
-import eu.kanade.tachiyomi.data.network.POST
-import eu.kanade.tachiyomi.data.source.model.*
-import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
-import okhttp3.Headers
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import java.util.*
-
-class Readmangatoday : ParsedOnlineSource() {
-
-    override val id: Long = 8
-
-    override val name = "ReadMangaToday"
-
-    override val baseUrl = "http://www.readmanga.today"
-
-    override val lang = "en"
-
-    override val supportsLatest = true
-
-    override val client: OkHttpClient get() = network.cloudflareClient
-
-    /**
-     * Search only returns data with this set
-     */
-    override fun headersBuilder() = Headers.Builder().apply {
-        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
-        add("X-Requested-With", "XMLHttpRequest")
-    }
-
-    override fun popularMangaRequest(page: Int): Request {
-        return GET("$baseUrl/hot-manga/$page", headers)
-    }
-
-    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): 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
-
-    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)
-        (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])
-                is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
-                is GenreList -> filter.state.forEach { genre ->
-                    when (genre.state) {
-                        Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
-                        Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
-                    }
-                }
-            }
-        }
-        return POST("$baseUrl/service/advanced_search", headers, builder.build())
-    }
-
-    override fun searchMangaSelector() = "div.style-list > div.box"
-
-    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): 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") -> SManga.ONGOING
-        status.contains("Completed") -> SManga.COMPLETED
-        else -> SManga.UNKNOWN
-    }
-
-    override fun chapterListSelector() = "ul.chp_lst > li"
-
-    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 {
-        val dateWords: List<String> = date.split(" ")
-
-        if (dateWords.size == 3) {
-            val timeAgo = Integer.parseInt(dateWords[0])
-            val date: Calendar = Calendar.getInstance()
-
-            if (dateWords[1].contains("Minute")) {
-                date.add(Calendar.MINUTE, -timeAgo)
-            } else if (dateWords[1].contains("Hour")) {
-                date.add(Calendar.HOUR_OF_DAY, -timeAgo)
-            } else if (dateWords[1].contains("Day")) {
-                date.add(Calendar.DAY_OF_YEAR, -timeAgo)
-            } else if (dateWords[1].contains("Week")) {
-                date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
-            } else if (dateWords[1].contains("Month")) {
-                date.add(Calendar.MONTH, -timeAgo)
-            } else if (dateWords[1].contains("Year")) {
-                date.add(Calendar.YEAR, -timeAgo)
-            }
-
-            return date.timeInMillis
-        }
-
-        return 0L
-    }
-
-    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")
-
-    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 Type : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
-    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
-
-    override fun getFilterList() = FilterList(
-            TextField("Author", "author-name"),
-            TextField("Artist", "artist-name"),
-            Type(),
-            Status(),
-            GenreList(getGenreList())
-    )
-
-    // [...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
-    private fun getGenreList() = listOf(
-            Genre("Action", 2),
-            Genre("Adventure", 4),
-            Genre("Comedy", 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("Martial Arts", 16),
-            Genre("Mature", 17),
-            Genre("Mecha", 18),
-            Genre("Mystery", 19),
-            Genre("One shot", 20),
-            Genre("Psychological", 21),
-            Genre("Romance", 22),
-            Genre("School Life", 23),
-            Genre("Sci-fi", 24),
-            Genre("Seinen", 25),
-            Genre("Shotacon", 26),
-            Genre("Shoujo", 27),
-            Genre("Shoujo Ai", 28),
-            Genre("Shounen", 29),
-            Genre("Shounen Ai", 30),
-            Genre("Slice of Life", 31),
-            Genre("Smut", 32),
-            Genre("Sports", 33),
-            Genre("Supernatural", 34),
-            Genre("Tragedy", 35),
-            Genre("Yaoi", 36),
-            Genre("Yuri", 37)
-    )
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.util.*
+
+class Readmangatoday : ParsedOnlineSource() {
+
+    override val id: Long = 8
+
+    override val name = "ReadMangaToday"
+
+    override val baseUrl = "http://www.readmanga.today"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    override val client: OkHttpClient get() = network.cloudflareClient
+
+    /**
+     * Search only returns data with this set
+     */
+    override fun headersBuilder() = Headers.Builder().apply {
+        add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
+        add("X-Requested-With", "XMLHttpRequest")
+    }
+
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/hot-manga/$page", headers)
+    }
+
+    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): 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
+
+    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)
+        (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])
+                is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
+                is GenreList -> filter.state.forEach { genre ->
+                    when (genre.state) {
+                        Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
+                        Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
+                    }
+                }
+            }
+        }
+        return POST("$baseUrl/service/advanced_search", headers, builder.build())
+    }
+
+    override fun searchMangaSelector() = "div.style-list > div.box"
+
+    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): 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") -> SManga.ONGOING
+        status.contains("Completed") -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    override fun chapterListSelector() = "ul.chp_lst > li"
+
+    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 {
+        val dateWords: List<String> = date.split(" ")
+
+        if (dateWords.size == 3) {
+            val timeAgo = Integer.parseInt(dateWords[0])
+            val date: Calendar = Calendar.getInstance()
+
+            if (dateWords[1].contains("Minute")) {
+                date.add(Calendar.MINUTE, -timeAgo)
+            } else if (dateWords[1].contains("Hour")) {
+                date.add(Calendar.HOUR_OF_DAY, -timeAgo)
+            } else if (dateWords[1].contains("Day")) {
+                date.add(Calendar.DAY_OF_YEAR, -timeAgo)
+            } else if (dateWords[1].contains("Week")) {
+                date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
+            } else if (dateWords[1].contains("Month")) {
+                date.add(Calendar.MONTH, -timeAgo)
+            } else if (dateWords[1].contains("Year")) {
+                date.add(Calendar.YEAR, -timeAgo)
+            }
+
+            return date.timeInMillis
+        }
+
+        return 0L
+    }
+
+    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")
+
+    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 Type : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
+
+    override fun getFilterList() = FilterList(
+            TextField("Author", "author-name"),
+            TextField("Artist", "artist-name"),
+            Type(),
+            Status(),
+            GenreList(getGenreList())
+    )
+
+    // [...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
+    private fun getGenreList() = listOf(
+            Genre("Action", 2),
+            Genre("Adventure", 4),
+            Genre("Comedy", 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("Martial Arts", 16),
+            Genre("Mature", 17),
+            Genre("Mecha", 18),
+            Genre("Mystery", 19),
+            Genre("One shot", 20),
+            Genre("Psychological", 21),
+            Genre("Romance", 22),
+            Genre("School Life", 23),
+            Genre("Sci-fi", 24),
+            Genre("Seinen", 25),
+            Genre("Shotacon", 26),
+            Genre("Shoujo", 27),
+            Genre("Shoujo Ai", 28),
+            Genre("Shounen", 29),
+            Genre("Shounen Ai", 30),
+            Genre("Slice of Life", 31),
+            Genre("Smut", 32),
+            Genre("Sports", 33),
+            Genre("Supernatural", 34),
+            Genre("Tragedy", 35),
+            Genre("Yaoi", 36),
+            Genre("Yuri", 37)
+    )
 }

+ 121 - 121
app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/german/WieManga.kt

@@ -1,122 +1,122 @@
-package eu.kanade.tachiyomi.data.source.online.german
-
-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 okhttp3.Request
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import java.text.SimpleDateFormat
-
-class WieManga : ParsedOnlineSource() {
-
-    override val id: Long = 10
-
-    override val name = "Wie Manga!"
-
-    override val baseUrl = "http://www.wiemanga.com"
-
-    override val lang = "de"
-
-    override val supportsLatest = true
-
-    override fun popularMangaSelector() = ".booklist td > div"
-
-    override fun latestUpdatesSelector() = ".booklist td > div"
-
-    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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun popularMangaNextPageSelector() = null
-
-    override fun latestUpdatesNextPageSelector() = null
-
-    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): 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): 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", "")
-        manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
-
-        if (manga.author == "RSS")
-            manga.author = null
-
-        if (manga.artist == "RSS")
-            manga.artist = null
-        return manga
-    }
-
-    override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
-
-    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(document: Document): List<Page> {
-        val pages = mutableListOf<Page>()
-
-        document.select("select#page").first().select("option").forEach {
-            pages.add(Page(pages.size, it.attr("value")))
-        }
-        return pages
-    }
-
-    override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
-
+package eu.kanade.tachiyomi.source.online.german
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+
+class WieManga : ParsedOnlineSource() {
+
+    override val id: Long = 10
+
+    override val name = "Wie Manga!"
+
+    override val baseUrl = "http://www.wiemanga.com"
+
+    override val lang = "de"
+
+    override val supportsLatest = true
+
+    override fun popularMangaSelector() = ".booklist td > div"
+
+    override fun latestUpdatesSelector() = ".booklist td > div"
+
+    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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun popularMangaNextPageSelector() = null
+
+    override fun latestUpdatesNextPageSelector() = null
+
+    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): 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): 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", "")
+        manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
+
+        if (manga.author == "RSS")
+            manga.author = null
+
+        if (manga.artist == "RSS")
+            manga.artist = null
+        return manga
+    }
+
+    override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
+
+    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(document: Document): List<Page> {
+        val pages = mutableListOf<Page>()
+
+        document.select("select#page").first().select("option").forEach {
+            pages.add(Page(pages.size, it.attr("value")))
+        }
+        return pages
+    }
+
+    override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
+
 }

+ 229 - 229
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mangachan.kt

@@ -1,230 +1,230 @@
-package eu.kanade.tachiyomi.data.source.online.russian
-
-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 : ParsedOnlineSource() {
-
-    override val id: Long = 7
-
-    override val name = "Mangachan"
-
-    override val baseUrl = "http://mangachan.me"
-
-    override val lang = "ru"
-
-    override val supportsLatest = true
-
-    override fun popularMangaRequest(page: Int): Request {
-        return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
-    }
-
-    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.filterIsInstance<Genre>().filter { !it.isIgnored() }
-            if (filt.isNotEmpty()) {
-                var genres = ""
-                filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
-                "$baseUrl/tags/${genres.dropLast(1)}"
-            } else {
-                "$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"
-
-    override fun latestUpdatesSelector() = "ul.area_rightNews li"
-
-    override fun searchMangaSelector() = popularMangaSelector()
-
-    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): 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
-
-    override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
-
-    override fun searchMangaNextPageSelector() = "a:contains(Далее)"
-
-    private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
-
-    override fun searchMangaParse(response: Response): MangasPage {
-        val document = response.asJsoup()
-        val mangas = document.select(searchMangaSelector()).map { element ->
-            searchMangaFromElement(element)
-        }
-
-        // 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): 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 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): 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): 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(',')
-
-        return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
-    }
-
-    override fun pageListParse(document: Document): List<Page> {
-        throw Exception("Not used")
-    }
-
-    override fun imageUrlParse(document: Document) = ""
-
-    private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
-
-    /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
-    *  { const link=el.getAttribute('href');const id=link.substr(6,link.length);
-    *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
-    *  on http://mangachan.me/
-    */
-    override fun getFilterList() = FilterList(
-            Genre("18 плюс"),
-            Genre("bdsm"),
-            Genre("арт"),
-            Genre("биография"),
-            Genre("боевик"),
-            Genre("боевые искусства"),
-            Genre("вампиры"),
-            Genre("веб"),
-            Genre("гарем"),
-            Genre("гендерная интрига"),
-            Genre("героическое фэнтези"),
-            Genre("детектив"),
-            Genre("дзёсэй"),
-            Genre("додзинси"),
-            Genre("драма"),
-            Genre("игра"),
-            Genre("инцест"),
-            Genre("искусство"),
-            Genre("история"),
-            Genre("киберпанк"),
-            Genre("кодомо"),
-            Genre("комедия"),
-            Genre("литРПГ"),
-            Genre("магия"),
-            Genre("махо-сёдзё"),
-            Genre("меха"),
-            Genre("мистика"),
-            Genre("музыка"),
-            Genre("научная фантастика"),
-            Genre("повседневность"),
-            Genre("постапокалиптика"),
-            Genre("приключения"),
-            Genre("психология"),
-            Genre("романтика"),
-            Genre("самурайский боевик"),
-            Genre("сборник"),
-            Genre("сверхъестественное"),
-            Genre("сказка"),
-            Genre("спорт"),
-            Genre("супергерои"),
-            Genre("сэйнэн"),
-            Genre("сёдзё"),
-            Genre("сёдзё-ай"),
-            Genre("сёнэн"),
-            Genre("сёнэн-ай"),
-            Genre("тентакли"),
-            Genre("трагедия"),
-            Genre("триллер"),
-            Genre("ужасы"),
-            Genre("фантастика"),
-            Genre("фурри"),
-            Genre("фэнтези"),
-            Genre("школа"),
-            Genre("эротика"),
-            Genre("юри"),
-            Genre("яой"),
-            Genre("ёнкома")
-    )
+package eu.kanade.tachiyomi.source.online.russian
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.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 : ParsedOnlineSource() {
+
+    override val id: Long = 7
+
+    override val name = "Mangachan"
+
+    override val baseUrl = "http://mangachan.me"
+
+    override val lang = "ru"
+
+    override val supportsLatest = true
+
+    override fun popularMangaRequest(page: Int): Request {
+        return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
+    }
+
+    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.filterIsInstance<Genre>().filter { !it.isIgnored() }
+            if (filt.isNotEmpty()) {
+                var genres = ""
+                filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
+                "$baseUrl/tags/${genres.dropLast(1)}"
+            } else {
+                "$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"
+
+    override fun latestUpdatesSelector() = "ul.area_rightNews li"
+
+    override fun searchMangaSelector() = popularMangaSelector()
+
+    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): 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
+
+    override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+    override fun searchMangaNextPageSelector() = "a:contains(Далее)"
+
+    private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
+
+    override fun searchMangaParse(response: Response): MangasPage {
+        val document = response.asJsoup()
+        val mangas = document.select(searchMangaSelector()).map { element ->
+            searchMangaFromElement(element)
+        }
+
+        // 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): 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 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): 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): 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(',')
+
+        return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
+    }
+
+    override fun pageListParse(document: Document): List<Page> {
+        throw Exception("Not used")
+    }
+
+    override fun imageUrlParse(document: Document) = ""
+
+    private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
+
+    /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
+    *  { const link=el.getAttribute('href');const id=link.substr(6,link.length);
+    *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
+    *  on http://mangachan.me/
+    */
+    override fun getFilterList() = FilterList(
+            Genre("18 плюс"),
+            Genre("bdsm"),
+            Genre("арт"),
+            Genre("биография"),
+            Genre("боевик"),
+            Genre("боевые искусства"),
+            Genre("вампиры"),
+            Genre("веб"),
+            Genre("гарем"),
+            Genre("гендерная интрига"),
+            Genre("героическое фэнтези"),
+            Genre("детектив"),
+            Genre("дзёсэй"),
+            Genre("додзинси"),
+            Genre("драма"),
+            Genre("игра"),
+            Genre("инцест"),
+            Genre("искусство"),
+            Genre("история"),
+            Genre("киберпанк"),
+            Genre("кодомо"),
+            Genre("комедия"),
+            Genre("литРПГ"),
+            Genre("магия"),
+            Genre("махо-сёдзё"),
+            Genre("меха"),
+            Genre("мистика"),
+            Genre("музыка"),
+            Genre("научная фантастика"),
+            Genre("повседневность"),
+            Genre("постапокалиптика"),
+            Genre("приключения"),
+            Genre("психология"),
+            Genre("романтика"),
+            Genre("самурайский боевик"),
+            Genre("сборник"),
+            Genre("сверхъестественное"),
+            Genre("сказка"),
+            Genre("спорт"),
+            Genre("супергерои"),
+            Genre("сэйнэн"),
+            Genre("сёдзё"),
+            Genre("сёдзё-ай"),
+            Genre("сёнэн"),
+            Genre("сёнэн-ай"),
+            Genre("тентакли"),
+            Genre("трагедия"),
+            Genre("триллер"),
+            Genre("ужасы"),
+            Genre("фантастика"),
+            Genre("фурри"),
+            Genre("фэнтези"),
+            Genre("школа"),
+            Genre("эротика"),
+            Genre("юри"),
+            Genre("яой"),
+            Genre("ёнкома")
+    )
 }

+ 183 - 183
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Mintmanga.kt

@@ -1,184 +1,184 @@
-package eu.kanade.tachiyomi.data.source.online.russian
-
-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
-import java.text.SimpleDateFormat
-import java.util.*
-import java.util.regex.Pattern
-
-class Mintmanga : ParsedOnlineSource() {
-
-    override val id: Long = 6
-
-    override val name = "Mintmanga"
-
-    override val baseUrl = "http://mintmanga.com"
-
-    override val lang = "ru"
-
-    override val supportsLatest = true
-
-    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 popularMangaSelector() = "div.desc"
-
-    override fun latestUpdatesSelector() = "div.desc"
-
-    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): 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    // max 200 results
-    override fun searchMangaNextPageSelector() = null
-
-    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 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): 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: SChapter, manga: SManga) {
-        chapter.chapter_number = -2f
-    }
-
-    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)
-        val trimmedHtml = html.substring(beginIndex, endIndex)
-
-        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): List<Page> {
-        throw Exception("Not used")
-    }
-
-    override fun imageUrlParse(document: Document) = ""
-
-    private class Genre(name: String, val id: String) : Filter.TriState(name)
-
-    /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
-    *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
-    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
-    *  on http://mintmanga.com/search
-    */
-    override fun getFilterList() = FilterList(
-            Genre("арт", "el_2220"),
-            Genre("бара", "el_1353"),
-            Genre("боевик", "el_1346"),
-            Genre("боевые искусства", "el_1334"),
-            Genre("вампиры", "el_1339"),
-            Genre("гарем", "el_1333"),
-            Genre("гендерная интрига", "el_1347"),
-            Genre("героическое фэнтези", "el_1337"),
-            Genre("детектив", "el_1343"),
-            Genre("дзёсэй", "el_1349"),
-            Genre("додзинси", "el_1332"),
-            Genre("драма", "el_1310"),
-            Genre("игра", "el_5229"),
-            Genre("история", "el_1311"),
-            Genre("киберпанк", "el_1351"),
-            Genre("комедия", "el_1328"),
-            Genre("меха", "el_1318"),
-            Genre("мистика", "el_1324"),
-            Genre("научная фантастика", "el_1325"),
-            Genre("повседневность", "el_1327"),
-            Genre("постапокалиптика", "el_1342"),
-            Genre("приключения", "el_1322"),
-            Genre("психология", "el_1335"),
-            Genre("романтика", "el_1313"),
-            Genre("самурайский боевик", "el_1316"),
-            Genre("сверхъестественное", "el_1350"),
-            Genre("сёдзё", "el_1314"),
-            Genre("сёдзё-ай", "el_1320"),
-            Genre("сёнэн", "el_1326"),
-            Genre("сёнэн-ай", "el_1330"),
-            Genre("спорт", "el_1321"),
-            Genre("сэйнэн", "el_1329"),
-            Genre("трагедия", "el_1344"),
-            Genre("триллер", "el_1341"),
-            Genre("ужасы", "el_1317"),
-            Genre("фантастика", "el_1331"),
-            Genre("фэнтези", "el_1323"),
-            Genre("школа", "el_1319"),
-            Genre("эротика", "el_1340"),
-            Genre("этти", "el_1354"),
-            Genre("юри", "el_1315"),
-            Genre("яой", "el_1336")
-    )
+package eu.kanade.tachiyomi.source.online.russian
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.regex.Pattern
+
+class Mintmanga : ParsedOnlineSource() {
+
+    override val id: Long = 6
+
+    override val name = "Mintmanga"
+
+    override val baseUrl = "http://mintmanga.com"
+
+    override val lang = "ru"
+
+    override val supportsLatest = true
+
+    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 popularMangaSelector() = "div.desc"
+
+    override fun latestUpdatesSelector() = "div.desc"
+
+    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): 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    // max 200 results
+    override fun searchMangaNextPageSelector() = null
+
+    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 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): 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: SChapter, manga: SManga) {
+        chapter.chapter_number = -2f
+    }
+
+    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)
+        val trimmedHtml = html.substring(beginIndex, endIndex)
+
+        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): List<Page> {
+        throw Exception("Not used")
+    }
+
+    override fun imageUrlParse(document: Document) = ""
+
+    private class Genre(name: String, val id: String) : Filter.TriState(name)
+
+    /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
+    *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
+    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
+    *  on http://mintmanga.com/search
+    */
+    override fun getFilterList() = FilterList(
+            Genre("арт", "el_2220"),
+            Genre("бара", "el_1353"),
+            Genre("боевик", "el_1346"),
+            Genre("боевые искусства", "el_1334"),
+            Genre("вампиры", "el_1339"),
+            Genre("гарем", "el_1333"),
+            Genre("гендерная интрига", "el_1347"),
+            Genre("героическое фэнтези", "el_1337"),
+            Genre("детектив", "el_1343"),
+            Genre("дзёсэй", "el_1349"),
+            Genre("додзинси", "el_1332"),
+            Genre("драма", "el_1310"),
+            Genre("игра", "el_5229"),
+            Genre("история", "el_1311"),
+            Genre("киберпанк", "el_1351"),
+            Genre("комедия", "el_1328"),
+            Genre("меха", "el_1318"),
+            Genre("мистика", "el_1324"),
+            Genre("научная фантастика", "el_1325"),
+            Genre("повседневность", "el_1327"),
+            Genre("постапокалиптика", "el_1342"),
+            Genre("приключения", "el_1322"),
+            Genre("психология", "el_1335"),
+            Genre("романтика", "el_1313"),
+            Genre("самурайский боевик", "el_1316"),
+            Genre("сверхъестественное", "el_1350"),
+            Genre("сёдзё", "el_1314"),
+            Genre("сёдзё-ай", "el_1320"),
+            Genre("сёнэн", "el_1326"),
+            Genre("сёнэн-ай", "el_1330"),
+            Genre("спорт", "el_1321"),
+            Genre("сэйнэн", "el_1329"),
+            Genre("трагедия", "el_1344"),
+            Genre("триллер", "el_1341"),
+            Genre("ужасы", "el_1317"),
+            Genre("фантастика", "el_1331"),
+            Genre("фэнтези", "el_1323"),
+            Genre("школа", "el_1319"),
+            Genre("эротика", "el_1340"),
+            Genre("этти", "el_1354"),
+            Genre("юри", "el_1315"),
+            Genre("яой", "el_1336")
+    )
 }

+ 182 - 182
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt → app/src/main/java/eu/kanade/tachiyomi/source/online/russian/Readmanga.kt

@@ -1,183 +1,183 @@
-package eu.kanade.tachiyomi.data.source.online.russian
-
-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
-import java.text.SimpleDateFormat
-import java.util.*
-import java.util.regex.Pattern
-
-class Readmanga : ParsedOnlineSource() {
-
-    override val id: Long = 5
-
-    override val name = "Readmanga"
-
-    override val baseUrl = "http://readmanga.me"
-
-    override val lang = "ru"
-
-    override val supportsLatest = true
-
-    override fun popularMangaSelector() = "div.desc"
-
-    override fun latestUpdatesSelector() = "div.desc"
-
-    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): 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): SManga {
-        return popularMangaFromElement(element)
-    }
-
-    // max 200 results
-    override fun searchMangaNextPageSelector() = null
-
-    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 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): 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: SChapter, manga: SManga) {
-        chapter.chapter_number = -2f
-    }
-
-    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)
-        val trimmedHtml = html.substring(beginIndex, endIndex)
-
-        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): List<Page> {
-        throw Exception("Not used")
-    }
-
-    override fun imageUrlParse(document: Document) = ""
-
-    private class Genre(name: String, val id: String) : Filter.TriState(name)
-
-    /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
-    *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
-    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
-    *  on http://readmanga.me/search
-    */
-    override fun getFilterList() = FilterList(
-            Genre("арт", "el_5685"),
-            Genre("боевик", "el_2155"),
-            Genre("боевые искусства", "el_2143"),
-            Genre("вампиры", "el_2148"),
-            Genre("гарем", "el_2142"),
-            Genre("гендерная интрига", "el_2156"),
-            Genre("героическое фэнтези", "el_2146"),
-            Genre("детектив", "el_2152"),
-            Genre("дзёсэй", "el_2158"),
-            Genre("додзинси", "el_2141"),
-            Genre("драма", "el_2118"),
-            Genre("игра", "el_2154"),
-            Genre("история", "el_2119"),
-            Genre("киберпанк", "el_8032"),
-            Genre("кодомо", "el_2137"),
-            Genre("комедия", "el_2136"),
-            Genre("махо-сёдзё", "el_2147"),
-            Genre("меха", "el_2126"),
-            Genre("мистика", "el_2132"),
-            Genre("научная фантастика", "el_2133"),
-            Genre("повседневность", "el_2135"),
-            Genre("постапокалиптика", "el_2151"),
-            Genre("приключения", "el_2130"),
-            Genre("психология", "el_2144"),
-            Genre("романтика", "el_2121"),
-            Genre("самурайский боевик", "el_2124"),
-            Genre("сверхъестественное", "el_2159"),
-            Genre("сёдзё", "el_2122"),
-            Genre("сёдзё-ай", "el_2128"),
-            Genre("сёнэн", "el_2134"),
-            Genre("сёнэн-ай", "el_2139"),
-            Genre("спорт", "el_2129"),
-            Genre("сэйнэн", "el_2138"),
-            Genre("трагедия", "el_2153"),
-            Genre("триллер", "el_2150"),
-            Genre("ужасы", "el_2125"),
-            Genre("фантастика", "el_2140"),
-            Genre("фэнтези", "el_2131"),
-            Genre("школа", "el_2127"),
-            Genre("этти", "el_2149"),
-            Genre("юри", "el_2123")
-    )
+package eu.kanade.tachiyomi.source.online.russian
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.*
+import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.regex.Pattern
+
+class Readmanga : ParsedOnlineSource() {
+
+    override val id: Long = 5
+
+    override val name = "Readmanga"
+
+    override val baseUrl = "http://readmanga.me"
+
+    override val lang = "ru"
+
+    override val supportsLatest = true
+
+    override fun popularMangaSelector() = "div.desc"
+
+    override fun latestUpdatesSelector() = "div.desc"
+
+    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): 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): SManga {
+        return popularMangaFromElement(element)
+    }
+
+    // max 200 results
+    override fun searchMangaNextPageSelector() = null
+
+    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 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): 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: SChapter, manga: SManga) {
+        chapter.chapter_number = -2f
+    }
+
+    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)
+        val trimmedHtml = html.substring(beginIndex, endIndex)
+
+        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): List<Page> {
+        throw Exception("Not used")
+    }
+
+    override fun imageUrlParse(document: Document) = ""
+
+    private class Genre(name: String, val id: String) : Filter.TriState(name)
+
+    /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
+    *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
+    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
+    *  on http://readmanga.me/search
+    */
+    override fun getFilterList() = FilterList(
+            Genre("арт", "el_5685"),
+            Genre("боевик", "el_2155"),
+            Genre("боевые искусства", "el_2143"),
+            Genre("вампиры", "el_2148"),
+            Genre("гарем", "el_2142"),
+            Genre("гендерная интрига", "el_2156"),
+            Genre("героическое фэнтези", "el_2146"),
+            Genre("детектив", "el_2152"),
+            Genre("дзёсэй", "el_2158"),
+            Genre("додзинси", "el_2141"),
+            Genre("драма", "el_2118"),
+            Genre("игра", "el_2154"),
+            Genre("история", "el_2119"),
+            Genre("киберпанк", "el_8032"),
+            Genre("кодомо", "el_2137"),
+            Genre("комедия", "el_2136"),
+            Genre("махо-сёдзё", "el_2147"),
+            Genre("меха", "el_2126"),
+            Genre("мистика", "el_2132"),
+            Genre("научная фантастика", "el_2133"),
+            Genre("повседневность", "el_2135"),
+            Genre("постапокалиптика", "el_2151"),
+            Genre("приключения", "el_2130"),
+            Genre("психология", "el_2144"),
+            Genre("романтика", "el_2121"),
+            Genre("самурайский боевик", "el_2124"),
+            Genre("сверхъестественное", "el_2159"),
+            Genre("сёдзё", "el_2122"),
+            Genre("сёдзё-ай", "el_2128"),
+            Genre("сёнэн", "el_2134"),
+            Genre("сёнэн-ай", "el_2139"),
+            Genre("спорт", "el_2129"),
+            Genre("сэйнэн", "el_2138"),
+            Genre("трагедия", "el_2153"),
+            Genre("триллер", "el_2150"),
+            Genre("ужасы", "el_2125"),
+            Genre("фантастика", "el_2140"),
+            Genre("фэнтези", "el_2131"),
+            Genre("школа", "el_2127"),
+            Genre("этти", "el_2149"),
+            Genre("юри", "el_2123")
+    )
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt

@@ -15,8 +15,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 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.source.model.FilterList
+import eu.kanade.tachiyomi.source.online.LoginSource
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaActivity

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt

@@ -1,8 +1,8 @@
 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.source.CatalogueSource
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers

+ 7 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -8,13 +8,13 @@ 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.Filter
-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.source.CatalogueSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.LoginSource
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.ui.catalogue.filter.*
 import rx.Observable

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt

@@ -1,8 +1,8 @@
 package eu.kanade.tachiyomi.ui.catalogue
 
 import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.data.source.model.MangasPage
-import eu.kanade.tachiyomi.data.source.model.SManga
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.SManga
 import rx.Observable
 
 /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt

@@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 
 open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt

@@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
 import eu.davidea.flexibleadapter.items.ISectionable
 import eu.davidea.viewholders.ExpandableViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.util.setVectorCompat
 
 class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt

@@ -9,7 +9,7 @@ import android.widget.TextView
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 
 class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt

@@ -1,7 +1,7 @@
 package eu.kanade.tachiyomi.ui.catalogue.filter
 
 import eu.davidea.flexibleadapter.items.ISectionable
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 
 class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt

@@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
 
 open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt

@@ -8,7 +8,7 @@ import android.view.ViewGroup
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 
 class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt

@@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
 import eu.davidea.flexibleadapter.items.ISectionable
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.util.setVectorCompat
 
 class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortItem.kt

@@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractSectionableItem
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.util.getResourceColor
 
 class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt

@@ -9,7 +9,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.widget.SimpleTextWatcher
 
 open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt

@@ -9,7 +9,7 @@ import android.widget.CheckedTextView
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.util.dpToPx
 import eu.kanade.tachiyomi.util.getResourceColor
 import eu.kanade.tachiyomi.R as TR

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt

@@ -7,7 +7,7 @@ import android.view.MenuItem
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 import eu.kanade.tachiyomi.util.plusAssign
 import kotlinx.android.synthetic.main.activity_main.*

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt

@@ -1,7 +1,7 @@
 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.source.CatalogueSource
+import eu.kanade.tachiyomi.source.model.MangasPage
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt

@@ -1,8 +1,8 @@
 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.model.FilterList
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.model.FilterList
 import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
 import eu.kanade.tachiyomi.data.download.DownloadManager
 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.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.combineLatest
 import eu.kanade.tachiyomi.util.isNullOrUnsubscribed

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt

@@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.source.Source
-import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.ui.manga.MangaEvent
 import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt

@@ -14,9 +14,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
 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.source.Source
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.manga.MangaActivity
 import eu.kanade.tachiyomi.util.getResourceColor

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt

@@ -4,8 +4,8 @@ import android.os.Bundle
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.source.Source
-import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.ui.manga.MangaEvent
 import eu.kanade.tachiyomi.util.SharedData

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt

@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader
 
 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.source.Source
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.online.fetchImageFromCacheThenNet
+import eu.kanade.tachiyomi.source.online.fetchPageListFromCacheThenNet
 import eu.kanade.tachiyomi.util.plusAssign
 import rx.Observable
 import rx.schedulers.Schedulers

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 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.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt

@@ -1,7 +1,7 @@
 package eu.kanade.tachiyomi.ui.reader
 
 import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 
 class ReaderChapter(c: Chapter) : Chapter by c {
 

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -13,9 +13,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.Track
 import eu.kanade.tachiyomi.data.download.DownloadManager
 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.source.SourceManager
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.data.track.TrackUpdateService
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base
 
 import com.davemorrissey.labs.subscaleview.decoder.*
 import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.reader.ReaderChapter

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt

@@ -4,7 +4,7 @@ import android.net.Uri
 import android.support.v4.content.ContextCompat
 import android.view.View
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import kotlinx.android.synthetic.main.page_decode_error.view.*
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt

@@ -10,7 +10,7 @@ import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 import com.hippo.unifile.UniFile
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt

@@ -6,7 +6,7 @@ import android.view.MotionEvent
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt

@@ -4,7 +4,7 @@ import android.support.v4.view.PagerAdapter
 import android.view.View
 import android.view.ViewGroup
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.util.inflate
 import eu.kanade.tachiyomi.widget.ViewPagerAdapter
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt

@@ -4,7 +4,7 @@ import android.support.v7.widget.RecyclerView
 import android.view.View
 import android.view.ViewGroup
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.util.inflate
 
 /**

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt

@@ -8,7 +8,7 @@ import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 import com.hippo.unifile.UniFile
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
 import eu.kanade.tachiyomi.util.inflate

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt

@@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView
 import android.view.*
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.widget.PreCachingLayoutManager

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt

@@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.DownloadService
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt

@@ -4,7 +4,7 @@ import android.view.ViewGroup
 import eu.davidea.flexibleadapter4.FlexibleAdapter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
-import eu.kanade.tachiyomi.data.source.SourceManager
+import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.util.inflate
 import uy.kohesive.injekt.injectLazy
 

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt

@@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.network.NetworkHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.util.plusAssign
 import eu.kanade.tachiyomi.util.toast
 import rx.Observable

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesFragment.kt

@@ -7,8 +7,8 @@ import android.support.v7.preference.XpPreferenceFragment
 import android.view.View
 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.online.OnlineSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference
 import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
 import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/util/ChapterSourceSync.kt

@@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.util
 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 eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import java.util.*
 
 /**

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginCheckBoxPreference.kt

@@ -6,8 +6,8 @@ import android.support.v7.preference.PreferenceViewHolder
 import android.util.AttributeSet
 import android.view.View
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.online.LoginSource
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import eu.kanade.tachiyomi.util.getResourceColor
 import eu.kanade.tachiyomi.util.setVectorCompat
 import kotlinx.android.synthetic.main.pref_item_source.view.*

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/widget/preference/SourceLoginDialog.kt

@@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.widget.preference
 import android.os.Bundle
 import android.view.View
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.source.Source
-import eu.kanade.tachiyomi.data.source.SourceManager
-import eu.kanade.tachiyomi.data.source.online.LoginSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.online.LoginSource
 import eu.kanade.tachiyomi.util.toast
 import kotlinx.android.synthetic.main.pref_account_login.view.*
 import rx.android.schedulers.AndroidSchedulers

+ 3 - 3
app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt

@@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.source.SourceManager
-import eu.kanade.tachiyomi.data.source.model.SChapter
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.online.OnlineSource
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test