commit
6603c0b990
@ -131,6 +131,9 @@ dependencies {
|
|||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.6.2'
|
compile 'com.google.code.gson:gson:2.6.2'
|
||||||
|
|
||||||
|
// YAML
|
||||||
|
compile 'org.yaml:snakeyaml:1.17'
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
||||||
|
|
||||||
@ -140,6 +143,9 @@ dependencies {
|
|||||||
// Parse HTML
|
// Parse HTML
|
||||||
compile 'org.jsoup:jsoup:1.9.1'
|
compile 'org.jsoup:jsoup:1.9.1'
|
||||||
|
|
||||||
|
// Changelog
|
||||||
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
||||||
|
@ -83,8 +83,8 @@ public class Manga implements Serializable {
|
|||||||
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
||||||
public static final int DOWNLOADED_MASK = 0x00000018;
|
public static final int DOWNLOADED_MASK = 0x00000018;
|
||||||
|
|
||||||
public static final int SORTING_NUMBER = 0x00000000;
|
public static final int SORTING_SOURCE = 0x00000000;
|
||||||
public static final int SORTING_SOURCE = 0x00000100;
|
public static final int SORTING_NUMBER = 0x00000100;
|
||||||
public static final int SORTING_MASK = 0x00000100;
|
public static final int SORTING_MASK = 0x00000100;
|
||||||
|
|
||||||
public static final int DISPLAY_NAME = 0x00000000;
|
public static final int DISPLAY_NAME = 0x00000000;
|
||||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
@ -108,7 +109,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
// Create a download object for every chapter and add them to the downloads queue
|
// Create a download object for every chapter and add them to the downloads queue
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
val source = sourceManager.get(manga.source)
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
|
||||||
|
|
||||||
// Used to avoid downloading chapters with the same name
|
// Used to avoid downloading chapters with the same name
|
||||||
val addedChapters = ArrayList<String>()
|
val addedChapters = ArrayList<String>()
|
||||||
@ -182,8 +183,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
DiskUtils.createDirectory(download.directory)
|
DiskUtils.createDirectory(download.directory)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null)
|
val pageListObservable = if (download.pages == null)
|
||||||
// Pull page list from network and add them to download object
|
// Pull page list from network and add them to download object
|
||||||
download.source.pullPageListFromNetwork(download.chapter.url)
|
download.source.fetchPageListFromNetwork(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
download.pages = pages
|
download.pages = pages
|
||||||
savePageList(download)
|
savePageList(download)
|
||||||
@ -199,7 +200,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
download.status = Download.DOWNLOADING
|
download.status = Download.DOWNLOADING
|
||||||
}
|
}
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
.concatMap { page -> getOrDownloadImage(page, download) }
|
.concatMap { page -> getOrDownloadImage(page, download) }
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
@ -251,9 +252,9 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save image on disk
|
// Save image on disk
|
||||||
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
|
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
return source.getImageProgressResponse(page)
|
return source.imageResponse(page)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
try {
|
try {
|
||||||
val file = File(directory, filename)
|
val file = File(directory, filename)
|
||||||
@ -376,7 +377,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||||
val mangaRelativePath = source.visibleName +
|
val mangaRelativePath = source.toString() +
|
||||||
File.separator +
|
File.separator +
|
||||||
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||||
|
|
||||||
|
@ -5,12 +5,12 @@ import java.util.List;
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource;
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
import rx.subjects.PublishSubject;
|
import rx.subjects.PublishSubject;
|
||||||
|
|
||||||
public class Download {
|
public class Download {
|
||||||
public Source source;
|
public OnlineSource source;
|
||||||
public Manga manga;
|
public Manga manga;
|
||||||
public Chapter chapter;
|
public Chapter chapter;
|
||||||
public List<Page> pages;
|
public List<Page> pages;
|
||||||
@ -29,7 +29,7 @@ public class Download {
|
|||||||
public static final int ERROR = 4;
|
public static final int ERROR = 4;
|
||||||
|
|
||||||
|
|
||||||
public Download(Source source, Manga manga, Chapter chapter) {
|
public Download(OnlineSource source, Manga manga, Chapter chapter) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.manga = manga;
|
this.manga = manga;
|
||||||
this.chapter = chapter;
|
this.chapter = chapter;
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.App
|
|||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -103,12 +104,11 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param manga the model.
|
||||||
*/
|
*/
|
||||||
fun getHeaders(manga: Manga): LazyHeaders {
|
fun getHeaders(manga: Manga): Headers {
|
||||||
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
return cachedHeaders.getOrPut(manga.source) {
|
||||||
val source = sourceManager.get(manga.source)!!
|
|
||||||
|
|
||||||
LazyHeaders.Builder().apply {
|
LazyHeaders.Builder().apply {
|
||||||
for ((key, value) in source.requestHeaders.toMultimap()) {
|
for ((key, value) in source.headers.toMultimap()) {
|
||||||
addHeader(key, value[0])
|
addHeader(key, value[0])
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -288,9 +289,8 @@ class LibraryUpdateService : Service() {
|
|||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
||||||
val source = sourceManager.get(manga.source)
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
||||||
return source!!
|
return source.fetchChapterList(manga)
|
||||||
.pullChaptersFromNetwork(manga.url)
|
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
object CloudflareScraper {
|
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
||||||
|
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var t,r,a,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var t,r,a,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||||
@ -17,7 +17,7 @@ object CloudflareScraper {
|
|||||||
//language=RegExp
|
//language=RegExp
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
fun request(chain: Interceptor.Chain, cookies: PersistentCookieStore): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
// Check if we already solved a challenge
|
// Check if we already solved a challenge
|
@ -32,20 +32,18 @@ class NetworkHelper(context: Context) {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cloudflareClient = defaultClient.newBuilder()
|
val cloudflareClient = defaultClient.newBuilder()
|
||||||
.addInterceptor { CloudflareScraper.request(it, cookies) }
|
.addInterceptor(CloudflareInterceptor(cookies))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cookies: PersistentCookieStore
|
val cookies: PersistentCookieStore
|
||||||
get() = cookieManager.store
|
get() = cookieManager.store
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
|
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
client.newCall(request).execute().apply { body().close() }
|
client.newCall(request).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun requestBody(request: Request, client: OkHttpClient = defaultClient): Observable<String> {
|
fun requestBody(request: Request, client: OkHttpClient = defaultClient): Observable<String> {
|
||||||
return Observable.fromCallable {
|
return Observable.fromCallable {
|
||||||
client.newCall(request).execute().body().string()
|
client.newCall(request).execute().body().string()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||||
|
|
||||||
|
@ -99,6 +99,8 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
|
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
||||||
|
|
||||||
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
|
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
package eu.kanade.tachiyomi.data.source
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.YamlOnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.english.*
|
import eu.kanade.tachiyomi.data.source.online.english.*
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
|
import eu.kanade.tachiyomi.data.source.online.russian.*
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
|
import org.yaml.snakeyaml.Yaml
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.io.File
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
val sourcesMap: HashMap<Int, Source>
|
|
||||||
|
|
||||||
val BATOTO = 1
|
val BATOTO = 1
|
||||||
val MANGAHERE = 2
|
val MANGAHERE = 2
|
||||||
val MANGAFOX = 3
|
val MANGAFOX = 3
|
||||||
@ -23,38 +25,45 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
val LAST_SOURCE = 8
|
val LAST_SOURCE = 8
|
||||||
|
|
||||||
init {
|
val sourcesMap = createSources()
|
||||||
sourcesMap = createSourcesMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun get(sourceKey: Int): Source? {
|
open fun get(sourceKey: Int): Source? {
|
||||||
return sourcesMap[sourceKey]
|
return sourcesMap[sourceKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSource(sourceKey: Int): Source? = when (sourceKey) {
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
||||||
BATOTO -> Batoto(context)
|
|
||||||
MANGAHERE -> Mangahere(context)
|
private fun createSource(id: Int): Source? = when (id) {
|
||||||
MANGAFOX -> Mangafox(context)
|
BATOTO -> Batoto(context, id)
|
||||||
KISSMANGA -> Kissmanga(context)
|
KISSMANGA -> Kissmanga(context, id)
|
||||||
READMANGA -> Readmanga(context)
|
MANGAHERE -> Mangahere(context, id)
|
||||||
MINTMANGA -> Mintmanga(context)
|
MANGAFOX -> Mangafox(context, id)
|
||||||
MANGACHAN -> Mangachan(context)
|
READMANGA -> Readmanga(context, id)
|
||||||
READMANGATODAY -> ReadMangaToday(context)
|
MINTMANGA -> Mintmanga(context, id)
|
||||||
|
MANGACHAN -> Mangachan(context, id)
|
||||||
|
READMANGATODAY -> Readmangatoday(context, id)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSourcesMap(): HashMap<Int, Source> {
|
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
||||||
val map = HashMap<Int, Source>()
|
|
||||||
for (i in 1..LAST_SOURCE) {
|
for (i in 1..LAST_SOURCE) {
|
||||||
val source = createSource(i)
|
createSource(i)?.let { put(i, it) }
|
||||||
if (source != null) {
|
}
|
||||||
source.id = i
|
|
||||||
map.put(i, source)
|
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||||
|
File.separator + context.getString(R.string.app_name), "parsers")
|
||||||
|
|
||||||
|
if (parsersDir.exists()) {
|
||||||
|
val yaml = Yaml()
|
||||||
|
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
||||||
|
try {
|
||||||
|
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||||
|
YamlOnlineSource(context, map).let { put(it.id, it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Error loading source from file. Bad format?")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSources(): List<Source> = ArrayList(sourcesMap.values)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.base;
|
|
||||||
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import okhttp3.Headers;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import rx.Observable;
|
|
||||||
|
|
||||||
public abstract class BaseSource {
|
|
||||||
|
|
||||||
private int id;
|
|
||||||
|
|
||||||
// Id of the source
|
|
||||||
public int getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(int id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Language getLang();
|
|
||||||
|
|
||||||
// Name of the source to display
|
|
||||||
public abstract String getName();
|
|
||||||
|
|
||||||
// Name of the source to display with the language
|
|
||||||
public String getVisibleName() {
|
|
||||||
return getName() + " (" + getLang().getCode() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base url of the source, like: http://example.com
|
|
||||||
public abstract String getBaseUrl();
|
|
||||||
|
|
||||||
// True if the source requires a login
|
|
||||||
public abstract boolean isLoginRequired();
|
|
||||||
|
|
||||||
// Return the initial popular mangas URL
|
|
||||||
protected abstract String getInitialPopularMangasUrl();
|
|
||||||
|
|
||||||
// Return the initial search url given a query
|
|
||||||
protected abstract String getInitialSearchUrl(String query);
|
|
||||||
|
|
||||||
// Get the popular list of mangas from the source's parsed document
|
|
||||||
protected abstract List<Manga> parsePopularMangasFromHtml(Document parsedHtml);
|
|
||||||
|
|
||||||
// Get the next popular page URL or null if it's the last
|
|
||||||
protected abstract String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page);
|
|
||||||
|
|
||||||
// Get the searched list of mangas from the source's parsed document
|
|
||||||
protected abstract List<Manga> parseSearchFromHtml(Document parsedHtml);
|
|
||||||
|
|
||||||
// Get the next search page URL or null if it's the last
|
|
||||||
protected abstract String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query);
|
|
||||||
|
|
||||||
// Given the URL of a manga and the result of the request, return the details of the manga
|
|
||||||
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml);
|
|
||||||
|
|
||||||
// Given the result of the request to mangas' chapters, return a list of chapters
|
|
||||||
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml);
|
|
||||||
|
|
||||||
// Given the result of the request to a chapter, return the list of URLs of the chapter
|
|
||||||
protected abstract List<String> parseHtmlToPageUrls(String unparsedHtml);
|
|
||||||
|
|
||||||
// Given the result of the request to a chapter's page, return the URL of the image of the page
|
|
||||||
protected abstract String parseHtmlToImageUrl(String unparsedHtml);
|
|
||||||
|
|
||||||
|
|
||||||
// Login related methods, shouldn't be overriden if the source doesn't require it
|
|
||||||
public Observable<Boolean> login(String username, String password) {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLogged() {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isAuthenticationSuccessful(Response response) {
|
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default headers, it can be overriden by children or just add new keys
|
|
||||||
protected Headers.Builder headersBuilder() {
|
|
||||||
Headers.Builder builder = new Headers.Builder();
|
|
||||||
builder.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)");
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getVisibleName();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.base;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public abstract class LoginSource extends Source {
|
|
||||||
|
|
||||||
public LoginSource(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoginRequired() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,448 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.*
|
||||||
|
import rx.Observable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
abstract class OnlineSource(context: Context) : Source {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network service.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var network: NetworkHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter cache.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var chapterCache: ChapterCache
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences helper.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var preferences: PreferencesHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
|
*/
|
||||||
|
abstract val baseUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language of the source.
|
||||||
|
*/
|
||||||
|
abstract val lang: Language
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headers used for requests.
|
||||||
|
*/
|
||||||
|
val headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default network client for doing requests.
|
||||||
|
*/
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = network.defaultClient
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Inject dependencies.
|
||||||
|
App.get(context).component.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.code})"
|
||||||
|
|
||||||
|
// Login source
|
||||||
|
|
||||||
|
open fun isLoginRequired() = false
|
||||||
|
|
||||||
|
open fun isLogged(): Boolean = throw Exception("Not implemented")
|
||||||
|
|
||||||
|
open fun login(username: String, password: String): Observable<Boolean>
|
||||||
|
= throw Exception("Not implemented")
|
||||||
|
|
||||||
|
open fun isAuthenticationSuccessful(response: Response): Boolean
|
||||||
|
= throw Exception("Not implemented")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param page the page object where the information will be saved, like the list of manga,
|
||||||
|
* the current page and the next page url.
|
||||||
|
*/
|
||||||
|
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = network
|
||||||
|
.request(popularMangaRequest(page), client)
|
||||||
|
.map { response ->
|
||||||
|
page.apply {
|
||||||
|
mangas = mutableListOf<Manga>()
|
||||||
|
popularMangaParse(response, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the popular manga given the page. Override only if it's needed to
|
||||||
|
* send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param page the page object.
|
||||||
|
*/
|
||||||
|
open protected fun popularMangaRequest(page: MangasPage): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = popularMangaInitialUrl()
|
||||||
|
}
|
||||||
|
return get(page.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url of the first page to popular manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaInitialUrl(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
||||||
|
* next page (if it has a next one) to [page].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param page the page object to be filled.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaParse(response: Response, page: MangasPage)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param page the page object where the information will be saved, like the list of manga,
|
||||||
|
* the current page and the next page url.
|
||||||
|
* @param query the search query.
|
||||||
|
*/
|
||||||
|
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = network
|
||||||
|
.request(searchMangaRequest(page, query), client)
|
||||||
|
.map { response ->
|
||||||
|
page.apply {
|
||||||
|
mangas = mutableListOf<Manga>()
|
||||||
|
searchMangaParse(response, this, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the search manga given the page. Override only if it's needed to
|
||||||
|
* send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param page the page object.
|
||||||
|
* @param query the search query.
|
||||||
|
*/
|
||||||
|
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = searchMangaInitialUrl(query)
|
||||||
|
}
|
||||||
|
return get(page.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url of the first page to popular manga.
|
||||||
|
*
|
||||||
|
* @param query the search query.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaInitialUrl(query: String): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
||||||
|
* next page (if it has a next one) to [page].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param page the page object to be filled.
|
||||||
|
* @param query the search query.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Manga): Observable<Manga> = network
|
||||||
|
.request(mangaDetailsRequest(manga), client)
|
||||||
|
.map { response ->
|
||||||
|
Manga.create(manga.url, id).apply {
|
||||||
|
mangaDetailsParse(response, this)
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for updating a manga. Override only if it's needed to override the url,
|
||||||
|
* send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param manga the manga to be updated.
|
||||||
|
*/
|
||||||
|
open protected fun mangaDetailsRequest(manga: Manga): Request {
|
||||||
|
return get(baseUrl + manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should fill [manga].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param manga the manga whose fields have to be filled.
|
||||||
|
*/
|
||||||
|
abstract protected fun mangaDetailsParse(response: Response, manga: Manga)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Manga): Observable<List<Chapter>> = network
|
||||||
|
.request(chapterListRequest(manga), client)
|
||||||
|
.map { response ->
|
||||||
|
mutableListOf<Chapter>().apply {
|
||||||
|
chapterListParse(response, this)
|
||||||
|
if (isEmpty()) {
|
||||||
|
throw Exception("No chapters found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Manga): Request {
|
||||||
|
return get(baseUrl + manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should fill [chapters].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param chapters the chapter list to be filled.
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterListParse(response: Response, chapters: MutableList<Chapter>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the page list for a chapter. It tries to return the page list from
|
||||||
|
* the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
|
||||||
|
*
|
||||||
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
|
*/
|
||||||
|
final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache
|
||||||
|
.getPageListFromCache(getChapterCacheKey(chapter))
|
||||||
|
.onErrorResumeNext { fetchPageListFromNetwork(chapter) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the page list for a chapter. Normally it's not needed to override
|
||||||
|
* this method.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
|
*/
|
||||||
|
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = network
|
||||||
|
.request(pageListRequest(chapter), client)
|
||||||
|
.map { response ->
|
||||||
|
mutableListOf<Page>().apply {
|
||||||
|
pageListParse(response, this)
|
||||||
|
if (isEmpty()) {
|
||||||
|
throw Exception("Page list is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Chapter): Request {
|
||||||
|
return get(baseUrl + chapter.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should fill [pages].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param pages the page list to be filled.
|
||||||
|
*/
|
||||||
|
abstract protected fun pageListParse(response: Response, pages: MutableList<Page>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key for the page list to be stored in [ChapterCache].
|
||||||
|
*/
|
||||||
|
private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 protected fun fetchImageUrl(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.LOAD_PAGE
|
||||||
|
return network
|
||||||
|
.request(imageUrlRequest(page), client)
|
||||||
|
.map { imageUrlParse(it) }
|
||||||
|
.doOnError { page.status = Page.ERROR }
|
||||||
|
.onErrorReturn { null }
|
||||||
|
.doOnNext { page.imageUrl = it }
|
||||||
|
.map { page }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site. It should return the absolute url to the source image.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun imageUrlParse(response: Response): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of the page with the downloaded image.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
final override fun fetchImage(page: Page): Observable<Page> =
|
||||||
|
if (page.imageUrl.isNullOrEmpty())
|
||||||
|
fetchImageUrl(page).flatMap { getCachedImage(it) }
|
||||||
|
else
|
||||||
|
getCachedImage(page)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the response of the source image.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
fun imageResponse(page: Page): Observable<Response> = network
|
||||||
|
.requestBodyProgress(imageRequest(page), page)
|
||||||
|
.doOnNext {
|
||||||
|
if (!it.isSuccessful) {
|
||||||
|
it.body().close()
|
||||||
|
throw RuntimeException("Not a valid response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 getCachedImage(page: Page): Observable<Page> {
|
||||||
|
val pageObservable = Observable.just(page)
|
||||||
|
if (page.imageUrl.isNullOrEmpty())
|
||||||
|
return pageObservable
|
||||||
|
|
||||||
|
return pageObservable
|
||||||
|
.flatMap {
|
||||||
|
if (!chapterCache.isImageInCache(page.imageUrl)) {
|
||||||
|
cacheImage(page)
|
||||||
|
} else {
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
page.imagePath = chapterCache.getImagePath(page.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 cacheImage(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
|
return imageResponse(page)
|
||||||
|
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) }
|
||||||
|
.map { page }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an absolute url from a href.
|
||||||
|
*
|
||||||
|
* Ex:
|
||||||
|
* href="http://example.com/foo" url="http://example.com" -> http://example.com/foo
|
||||||
|
* href="/mypath" url="http://example.com/foo" -> http://example.com/mypath
|
||||||
|
* href="bar" url="http://example.com/foo" -> http://example.com/bar
|
||||||
|
* href="bar" url="http://example.com/foo/" -> http://example.com/foo/bar
|
||||||
|
*
|
||||||
|
* @param href the href attribute from the html.
|
||||||
|
* @param url the requested url.
|
||||||
|
*/
|
||||||
|
fun getAbsoluteUrl(href: String, url: HttpUrl) = when {
|
||||||
|
href.startsWith("http://") || href.startsWith("https://") -> href
|
||||||
|
href.startsWith("/") -> url.newBuilder().encodedPath("/").fragment(null).query(null)
|
||||||
|
.toString() + href.substring(1)
|
||||||
|
else -> url.toString().substringBeforeLast('/') + "/$href"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
|
||||||
|
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||||
|
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||||
|
|
||||||
|
fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
|
||||||
|
.filter { it.imageUrl.isNullOrEmpty() }
|
||||||
|
.concatMap { fetchImageUrl(it) }
|
||||||
|
|
||||||
|
fun savePageList(chapter: Chapter, pages: List<Page>?) {
|
||||||
|
if (pages != null) {
|
||||||
|
chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overridable method to allow custom parsing.
|
||||||
|
open fun parseChapterNumber(chapter: Chapter) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and fills [page].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param page the page object to be filled.
|
||||||
|
*/
|
||||||
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(popularMangaSelector())) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@ParsedOnlineSource.id
|
||||||
|
popularMangaFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popularMangaNextPageSelector()?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally safe to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [popularMangaSelector].
|
||||||
|
* @param manga the manga to fill.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and fills [page].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param page the page object to be filled.
|
||||||
|
* @param query the search query.
|
||||||
|
*/
|
||||||
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(searchMangaSelector())) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@ParsedOnlineSource.id
|
||||||
|
searchMangaFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchMangaNextPageSelector()?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally safe to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [searchMangaSelector].
|
||||||
|
* @param manga the manga to fill.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and fills the details of [manga].
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param manga the manga to fill.
|
||||||
|
*/
|
||||||
|
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||||
|
mangaDetailsParse(Jsoup.parse(response.body().string()), manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the details of [manga] from the given [document].
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
* @param manga the manga to fill.
|
||||||
|
*/
|
||||||
|
abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and fills the chapter list.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param chapters the list of chapters to fill.
|
||||||
|
*/
|
||||||
|
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
|
||||||
|
for (element in document.select(chapterListSelector())) {
|
||||||
|
Chapter.create().apply {
|
||||||
|
chapterFromElement(element, this)
|
||||||
|
chapters.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterListSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills [chapter] with the given [element].
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [chapterListSelector].
|
||||||
|
* @param chapter the chapter to fill.
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and fills the page list.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
* @param pages the list of pages to fill.
|
||||||
|
*/
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
|
pageListParse(Jsoup.parse(response.body().string()), pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills [pages] from the given [document].
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
* @param pages the list of pages to fill.
|
||||||
|
*/
|
||||||
|
abstract protected fun pageListParse(document: Document, pages: MutableList<Page>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(Jsoup.parse(response.body().string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url to the source image from the document.
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
abstract protected fun imageUrlParse(document: Document): String
|
||||||
|
|
||||||
|
}
|
@ -1,228 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.base
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.network.get
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
abstract class Source(context: Context) : BaseSource() {
|
/**
|
||||||
|
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||||
|
*/
|
||||||
|
interface Source {
|
||||||
|
|
||||||
@Inject protected lateinit var networkService: NetworkHelper
|
/**
|
||||||
@Inject protected lateinit var chapterCache: ChapterCache
|
* Id for the source. Must be unique.
|
||||||
@Inject protected lateinit var prefs: PreferencesHelper
|
*/
|
||||||
|
val id: Int
|
||||||
|
|
||||||
val requestHeaders by lazy { headersBuilder().build() }
|
/**
|
||||||
|
* Name of the source.
|
||||||
|
*/
|
||||||
|
val name: String
|
||||||
|
|
||||||
init {
|
/**
|
||||||
App.get(context).component.inject(this)
|
* Returns an observable with the updated details for a manga.
|
||||||
}
|
*
|
||||||
|
* @param manga the manga to update.
|
||||||
|
*/
|
||||||
|
fun fetchMangaDetails(manga: Manga): Observable<Manga>
|
||||||
|
|
||||||
open val networkClient: OkHttpClient
|
/**
|
||||||
get() = networkService.defaultClient
|
* Returns an observable with all the available chapters for a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to update.
|
||||||
|
*/
|
||||||
|
fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
|
||||||
|
|
||||||
override fun isLoginRequired(): Boolean {
|
/**
|
||||||
return false
|
* Returns an observable with the list of pages a chapter has.
|
||||||
}
|
*
|
||||||
|
* @param chapter the chapter.
|
||||||
|
*/
|
||||||
|
fun fetchPageList(chapter: Chapter): Observable<List<Page>>
|
||||||
|
|
||||||
protected fun popularMangaRequest(page: MangasPage): Request {
|
/**
|
||||||
if (page.page == 1) {
|
* Returns an observable with the path of the image.
|
||||||
page.url = initialPopularMangasUrl
|
*
|
||||||
}
|
* @param page the page.
|
||||||
|
*/
|
||||||
return get(page.url, requestHeaders)
|
fun fetchImage(page: Page): Observable<Page>
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun searchMangaRequest(page: MangasPage, query: String): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = getInitialSearchUrl(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
return get(page.url, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun mangaDetailsRequest(mangaUrl: String): Request {
|
|
||||||
return get(baseUrl + mangaUrl, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun chapterListRequest(mangaUrl: String): Request {
|
|
||||||
return get(baseUrl + mangaUrl, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun pageListRequest(chapterUrl: String): Request {
|
|
||||||
return get(baseUrl + chapterUrl, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun imageUrlRequest(page: Page): Request {
|
|
||||||
return get(page.url, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun imageRequest(page: Page): Request {
|
|
||||||
return get(page.imageUrl, requestHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the most popular mangas from the source
|
|
||||||
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
|
|
||||||
return networkService.requestBody(popularMangaRequest(page), networkClient)
|
|
||||||
.map { Jsoup.parse(it) }
|
|
||||||
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
|
|
||||||
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
|
|
||||||
.map { response -> page }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mangas from the source with a query
|
|
||||||
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
|
|
||||||
return networkService.requestBody(searchMangaRequest(page, query), networkClient)
|
|
||||||
.map { Jsoup.parse(it) }
|
|
||||||
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
|
|
||||||
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
|
|
||||||
.map { response -> page }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get manga details from the source
|
|
||||||
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
|
|
||||||
return networkService.requestBody(mangaDetailsRequest(mangaUrl), networkClient)
|
|
||||||
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get chapter list of a manga from the source
|
|
||||||
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
|
|
||||||
return networkService.requestBody(chapterListRequest(mangaUrl), networkClient)
|
|
||||||
.flatMap { unparsedHtml ->
|
|
||||||
val chapters = parseHtmlToChapters(unparsedHtml)
|
|
||||||
if (!chapters.isEmpty())
|
|
||||||
Observable.just(chapters)
|
|
||||||
else
|
|
||||||
Observable.error(Exception("No chapters found"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getCachedPageListOrPullFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
|
||||||
return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
|
|
||||||
.onErrorResumeNext { pullPageListFromNetwork(chapterUrl) }
|
|
||||||
.onBackpressureBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
|
||||||
return networkService.requestBody(pageListRequest(chapterUrl), networkClient)
|
|
||||||
.flatMap { unparsedHtml ->
|
|
||||||
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
|
|
||||||
if (!pages.isEmpty())
|
|
||||||
Observable.just(parseFirstPage(pages, unparsedHtml))
|
|
||||||
else
|
|
||||||
Observable.error(Exception("Page list is empty"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
|
||||||
return Observable.from(pages)
|
|
||||||
.filter { page -> page.imageUrl != null }
|
|
||||||
.mergeWith(getRemainingImageUrlsFromPageList(pages))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the URLs of the images of a chapter
|
|
||||||
open fun getRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
|
||||||
return Observable.from(pages)
|
|
||||||
.filter { page -> page.imageUrl == null }
|
|
||||||
.concatMap { getImageUrlFromPage(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getImageUrlFromPage(page: Page): Observable<Page> {
|
|
||||||
page.status = Page.LOAD_PAGE
|
|
||||||
return networkService.requestBody(imageUrlRequest(page), networkClient)
|
|
||||||
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
|
|
||||||
.onErrorResumeNext { e ->
|
|
||||||
page.status = Page.ERROR
|
|
||||||
Observable.just<String>(null)
|
|
||||||
}
|
|
||||||
.flatMap { imageUrl ->
|
|
||||||
page.imageUrl = imageUrl
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getCachedImage(page: Page): Observable<Page> {
|
|
||||||
val pageObservable = Observable.just(page)
|
|
||||||
if (page.imageUrl == null)
|
|
||||||
return pageObservable
|
|
||||||
|
|
||||||
return pageObservable
|
|
||||||
.flatMap { p ->
|
|
||||||
if (!chapterCache.isImageInCache(page.imageUrl)) {
|
|
||||||
return@flatMap cacheImage(page)
|
|
||||||
}
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
.flatMap { p ->
|
|
||||||
page.imagePath = chapterCache.getImagePath(page.imageUrl)
|
|
||||||
page.status = Page.READY
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
.onErrorResumeNext { e ->
|
|
||||||
page.status = Page.ERROR
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cacheImage(page: Page): Observable<Page> {
|
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
|
||||||
return getImageProgressResponse(page)
|
|
||||||
.flatMap { resp ->
|
|
||||||
chapterCache.putImageToCache(page.imageUrl, resp, prefs.reencodeImage())
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getImageProgressResponse(page: Page): Observable<Response> {
|
|
||||||
return networkService.requestBodyProgress(imageRequest(page), page)
|
|
||||||
.doOnNext {
|
|
||||||
if (!it.isSuccessful) {
|
|
||||||
it.body().close()
|
|
||||||
throw RuntimeException("Not a valid response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePageList(chapterUrl: String, pages: List<Page>?) {
|
|
||||||
if (pages != null)
|
|
||||||
chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun convertToPages(pageUrls: List<String>): List<Page> {
|
|
||||||
val pages = ArrayList<Page>()
|
|
||||||
for (i in pageUrls.indices) {
|
|
||||||
pages.add(Page(i, pageUrls[i]))
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
|
|
||||||
val firstImage = parseHtmlToImageUrl(unparsedHtml)
|
|
||||||
pages[0].imageUrl = firstImage
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun getChapterCacheKey(chapterUrl: String): String {
|
|
||||||
return "$id$chapterUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overridable method to allow custom parsing.
|
|
||||||
open fun parseChapterNumber(chapter: Chapter) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
|
import eu.kanade.tachiyomi.data.network.post
|
||||||
|
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
|
||||||
|
|
||||||
|
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.toUpperCase().let { code ->
|
||||||
|
getLanguages().find { code == it.code }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override val client = when(map.client) {
|
||||||
|
"cloudflare" -> network.cloudflareClient
|
||||||
|
else -> network.defaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
override val id = map.id.let {
|
||||||
|
if (it is Int) it else (lang.code.hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: MangasPage): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = popularMangaInitialUrl()
|
||||||
|
}
|
||||||
|
return when (map.popular.method?.toLowerCase()) {
|
||||||
|
"post" -> post(page.url, headers, map.popular.createForm())
|
||||||
|
else -> get(page.url, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = map.popular.url
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(map.popular.manga_css)) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@YamlOnlineSource.id
|
||||||
|
title = element.text()
|
||||||
|
setUrl(element.attr("href"))
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.popular.next_url_css?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = searchMangaInitialUrl(query)
|
||||||
|
}
|
||||||
|
return when (map.search.method?.toLowerCase()) {
|
||||||
|
"post" -> post(page.url, headers, map.search.createForm())
|
||||||
|
else -> get(page.url, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(map.search.manga_css)) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@YamlOnlineSource.id
|
||||||
|
title = element.text()
|
||||||
|
setUrl(element.attr("href"))
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.search.next_url_css?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
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) ?: Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
with(map.chapters) {
|
||||||
|
val pool = emptyMap<String, Element>()
|
||||||
|
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
||||||
|
|
||||||
|
for (element in document.select(chapter_css)) {
|
||||||
|
val chapter = Chapter.create()
|
||||||
|
element.select(title).first().let {
|
||||||
|
chapter.name = it.text()
|
||||||
|
chapter.setUrl(it.attr("href"))
|
||||||
|
}
|
||||||
|
val dateElement = element.select(date?.select).first()
|
||||||
|
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
||||||
|
chapters.add(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
with(map.pages) {
|
||||||
|
val url = response.request().url().toString()
|
||||||
|
pages_css?.let {
|
||||||
|
for (element in document.select(it)) {
|
||||||
|
val value = element.attr(pages_attr)
|
||||||
|
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value
|
||||||
|
pages.add(Page(pages.size, pageUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((i, element) in document.select(image_css).withIndex()) {
|
||||||
|
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
||||||
|
page.imageUrl = element.attr(image_attr).let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
return with(map.pages) {
|
||||||
|
document.select(image_css).first().attr(image_attr).let {
|
||||||
|
getAbsoluteUrl(it, response.request().url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,214 @@
|
|||||||
|
@file:Suppress("UNCHECKED_CAST")
|
||||||
|
|
||||||
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
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 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 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 Manga.COMPLETED
|
||||||
|
}
|
||||||
|
ongoing?.let {
|
||||||
|
if (text.contains(it)) return Manga.ONGOING
|
||||||
|
}
|
||||||
|
licensed?.let {
|
||||||
|
if (text.contains(it)) return Manga.LICENSED
|
||||||
|
}
|
||||||
|
return Manga.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_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_css: String by map
|
||||||
|
|
||||||
|
val image_attr: String
|
||||||
|
get() = map["image_attr"] as? String ?: "src"
|
||||||
|
|
||||||
|
}
|
@ -1,393 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.network.ReqKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.LoginSource;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
import okhttp3.Cookie;
|
|
||||||
import okhttp3.FormBody;
|
|
||||||
import okhttp3.Headers;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.functions.Func1;
|
|
||||||
|
|
||||||
public class Batoto extends LoginSource {
|
|
||||||
|
|
||||||
public static final String NAME = "Batoto";
|
|
||||||
public static final String BASE_URL = "http://bato.to";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%s";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
|
|
||||||
public static final String CHAPTER_URL = BASE_URL + "/areader?id=%s&p=1";
|
|
||||||
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
|
|
||||||
public static final String MANGA_URL = BASE_URL + "/comic_pop?id=%s";
|
|
||||||
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=login";
|
|
||||||
|
|
||||||
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
private final Pattern datePattern;
|
|
||||||
private final Map<String, Integer> dateFields;
|
|
||||||
|
|
||||||
public Batoto(Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*");
|
|
||||||
dateFields = new HashMap<String, Integer>() {{
|
|
||||||
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);
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getEN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Headers.Builder headersBuilder() {
|
|
||||||
Headers.Builder builder = super.headersBuilder();
|
|
||||||
builder.add("Cookie", "lang_option=English");
|
|
||||||
builder.add("Referer", "http://bato.to/reader");
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getInitialPopularMangasUrl() {
|
|
||||||
return String.format(POPULAR_MANGAS_URL, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Request mangaDetailsRequest(String mangaUrl) {
|
|
||||||
String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf('r') + 1);
|
|
||||||
return ReqKt.get(String.format(MANGA_URL, mangaId), getRequestHeaders());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Request pageListRequest(String pageUrl) {
|
|
||||||
String id = pageUrl.substring(pageUrl.indexOf('#') + 1);
|
|
||||||
return ReqKt.get(String.format(CHAPTER_URL, id), getRequestHeaders());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Request imageUrlRequest(Page page) {
|
|
||||||
String pageUrl = page.getUrl();
|
|
||||||
int start = pageUrl.indexOf('#') + 1;
|
|
||||||
int end = pageUrl.indexOf('_', start);
|
|
||||||
String id = pageUrl.substring(start, end);
|
|
||||||
return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), getRequestHeaders());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Manga> parseMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
if (!parsedHtml.text().contains("No (more) comics found!")) {
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("tr:not([id]):not([class])")) {
|
|
||||||
Manga manga = constructMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(manga);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
return parseMangasFromHtml(parsedHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
Element next = Parser.element(parsedHtml, "#show_more_row");
|
|
||||||
return next != null ? String.format(POPULAR_MANGAS_URL, page.page + 1) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
return parseMangasFromHtml(parsedHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "a[href^=http://bato.to]");
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text().trim();
|
|
||||||
}
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
Element next = Parser.element(parsedHtml, "#show_more_row");
|
|
||||||
return next != null ? String.format(SEARCH_URL, query, page.page + 1) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
Element tbody = parsedDocument.select("tbody").first();
|
|
||||||
Element artistElement = tbody.select("tr:contains(Author/Artist:)").first();
|
|
||||||
Elements genreElements = tbody.select("tr:contains(Genres:) img");
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
manga.author = Parser.text(artistElement, "td:eq(1)");
|
|
||||||
manga.artist = Parser.text(artistElement, "td:eq(2)", manga.author);
|
|
||||||
manga.description = Parser.text(tbody, "tr:contains(Description:) > td:eq(1)");
|
|
||||||
manga.thumbnail_url = Parser.src(parsedDocument, "img[src^=http://img.bato.to/forums/uploads/]");
|
|
||||||
manga.status = parseStatus(Parser.text(parsedDocument, "tr:contains(Status:) > td:eq(1)"));
|
|
||||||
|
|
||||||
if (!genreElements.isEmpty()) {
|
|
||||||
List<String> genres = new ArrayList<>();
|
|
||||||
for (Element element : genreElements) {
|
|
||||||
genres.add(element.attr("alt"));
|
|
||||||
}
|
|
||||||
manga.genre = TextUtils.join(", ", genres);
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
switch (status) {
|
|
||||||
case "Ongoing":
|
|
||||||
return Manga.ONGOING;
|
|
||||||
case "Complete":
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
default:
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
Matcher matcher = staffNotice.matcher(unparsedHtml);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String notice = Html.fromHtml(matcher.group(1)).toString().trim();
|
|
||||||
throw new RuntimeException(notice);
|
|
||||||
}
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
Elements chapterElements = parsedDocument.select("tr.row.lang_English.chapter_row");
|
|
||||||
for (Element chapterElement : chapterElements) {
|
|
||||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(chapter);
|
|
||||||
}
|
|
||||||
return chapterList;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = chapterElement.select("a[href^=http://bato.to/reader").first();
|
|
||||||
Element dateElement = chapterElement.select("td").get(4);
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
String fieldUrl = urlElement.attr("href");
|
|
||||||
chapter.setUrl(fieldUrl);
|
|
||||||
chapter.name = urlElement.text().trim();
|
|
||||||
}
|
|
||||||
if (dateElement != null) {
|
|
||||||
chapter.date_upload = parseDateFromElement(dateElement);
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("WrongConstant")
|
|
||||||
private long parseDateFromElement(Element dateElement) {
|
|
||||||
String dateAsString = dateElement.text();
|
|
||||||
|
|
||||||
Date date;
|
|
||||||
try {
|
|
||||||
date = new SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
Matcher m = datePattern.matcher(dateAsString);
|
|
||||||
|
|
||||||
if (m.matches()) {
|
|
||||||
String number = m.group(1);
|
|
||||||
int amount = number.contains("A") ? 1 : Integer.parseInt(m.group(1));
|
|
||||||
String unit = m.group(2);
|
|
||||||
|
|
||||||
Calendar cal = Calendar.getInstance();
|
|
||||||
cal.add(dateFields.get(unit), -amount);
|
|
||||||
date = cal.getTime();
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return date.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
List<String> pageUrlList = new ArrayList<>();
|
|
||||||
|
|
||||||
Element selectElement = Parser.element(parsedDocument, "#page_select");
|
|
||||||
if (selectElement != null) {
|
|
||||||
for (Element pageUrlElement : selectElement.select("option")) {
|
|
||||||
pageUrlList.add(pageUrlElement.attr("value"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For webtoons in one page
|
|
||||||
for (int i = 0; i < parsedDocument.select("div > img").size(); i++) {
|
|
||||||
pageUrlList.add("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageUrlList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
|
||||||
if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
|
|
||||||
String firstImage = parseHtmlToImageUrl(unparsedHtml);
|
|
||||||
pages.get(0).setImageUrl(firstImage);
|
|
||||||
} else {
|
|
||||||
// For webtoons in one page
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
Elements imageUrls = parsedDocument.select("div > img");
|
|
||||||
for (int i = 0; i < pages.size(); i++) {
|
|
||||||
pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Page>) pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<img id=\"comic_page\"");
|
|
||||||
int endIndex = unparsedHtml.indexOf("</a>", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
Element imageElement = parsedDocument.getElementById("comic_page");
|
|
||||||
return imageElement.attr("src");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Observable<Boolean> login(final String username, final String password) {
|
|
||||||
return getNetworkService().requestBody(ReqKt.get(LOGIN_URL, getRequestHeaders()))
|
|
||||||
.flatMap(new Func1<String, Observable<Response>>() {
|
|
||||||
@Override
|
|
||||||
public Observable<Response> call(String response) {return doLogin(response, username, password);}
|
|
||||||
})
|
|
||||||
.map(new Func1<Response, Boolean>() {
|
|
||||||
@Override
|
|
||||||
public Boolean call(Response resp) {return isAuthenticationSuccessful(resp);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<Response> doLogin(String response, String username, String password) {
|
|
||||||
Document doc = Jsoup.parse(response);
|
|
||||||
Element form = doc.select("#login").first();
|
|
||||||
String postUrl = form.attr("action");
|
|
||||||
|
|
||||||
FormBody.Builder formBody = new FormBody.Builder();
|
|
||||||
Element authKey = form.select("input[name=auth_key]").first();
|
|
||||||
|
|
||||||
formBody.add(authKey.attr("name"), authKey.attr("value"));
|
|
||||||
formBody.add("ips_username", username);
|
|
||||||
formBody.add("ips_password", password);
|
|
||||||
formBody.add("invisible", "1");
|
|
||||||
formBody.add("rememberMe", "1");
|
|
||||||
|
|
||||||
return getNetworkService().request(ReqKt.post(postUrl, getRequestHeaders(), formBody.build()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isAuthenticationSuccessful(Response response) {
|
|
||||||
return response.priorResponse() != null && response.priorResponse().code() == 302;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLogged() {
|
|
||||||
try {
|
|
||||||
for (Cookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL))) {
|
|
||||||
if (cookie.name().equals("pass_hash"))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
|
|
||||||
Observable<List<Chapter>> observable;
|
|
||||||
String username = getPrefs().sourceUsername(this);
|
|
||||||
String password = getPrefs().sourcePassword(this);
|
|
||||||
if (username.isEmpty() && password.isEmpty()) {
|
|
||||||
observable = Observable.error(new Exception("User not logged"));
|
|
||||||
}
|
|
||||||
else if (!isLogged()) {
|
|
||||||
observable = login(username, password)
|
|
||||||
.flatMap(new Func1<Boolean, Observable<? extends List<Chapter>>>() {
|
|
||||||
@Override
|
|
||||||
public Observable<? extends List<Chapter>> call(Boolean result) {return Batoto.super.pullChaptersFromNetwork(mangaUrl);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
observable = super.pullChaptersFromNetwork(mangaUrl);
|
|
||||||
}
|
|
||||||
return observable;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,271 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.Html
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
|
import eu.kanade.tachiyomi.data.network.post
|
||||||
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Batoto"
|
||||||
|
|
||||||
|
override val baseUrl = "http://bato.to"
|
||||||
|
|
||||||
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
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")
|
||||||
|
.add("Referer", "http://bato.to/reader")
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: Manga): Request {
|
||||||
|
val mangaId = manga.url.substringAfterLast("r")
|
||||||
|
return get("$baseUrl/comic_pop?id=$mangaId", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: Chapter): Request {
|
||||||
|
val id = chapter.url.substringAfterLast("#")
|
||||||
|
return get("$baseUrl/areader?id=$id&p=1", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(popularMangaSelector())) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@Batoto.id
|
||||||
|
popularMangaFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let {
|
||||||
|
"$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "tr:not([id]):not([class])"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("a[href^=http://bato.to]").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text().trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
for (element in document.select(searchMangaSelector())) {
|
||||||
|
Manga().apply {
|
||||||
|
source = this@Batoto.id
|
||||||
|
searchMangaFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||||
|
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val tbody = document.select("tbody").first()
|
||||||
|
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
||||||
|
|
||||||
|
manga.author = artistElement.select("td:eq(1)").first()?.text()
|
||||||
|
manga.artist = artistElement.select("td:eq(2)").first()?.text() ?: manga.author
|
||||||
|
manga.description = tbody.select("tr:contains(Description:) > td:eq(1)").first()?.text()
|
||||||
|
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
|
||||||
|
manga.status = parseStatus(document.select("tr:contains(Status:) > td:eq(1)").first()?.text())
|
||||||
|
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String?) = when (status) {
|
||||||
|
"Ongoing" -> Manga.ONGOING
|
||||||
|
"Complete" -> Manga.COMPLETED
|
||||||
|
else -> Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||||
|
val body = response.body().string()
|
||||||
|
val matcher = staffNotice.matcher(body)
|
||||||
|
if (matcher.find()) {
|
||||||
|
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
||||||
|
throw RuntimeException(notice)
|
||||||
|
}
|
||||||
|
|
||||||
|
val document = Jsoup.parse(body)
|
||||||
|
|
||||||
|
for (element in document.select(chapterListSelector())) {
|
||||||
|
Chapter.create().apply {
|
||||||
|
chapterFromElement(element, this)
|
||||||
|
chapters.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||||
|
|
||||||
|
chapter.setUrl(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||||
|
parseDateFromElement(it)
|
||||||
|
} ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDateFromElement(dateElement: Element): Long {
|
||||||
|
val dateAsString = dateElement.text()
|
||||||
|
|
||||||
|
val 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 pageListParse(document: Document, pages: MutableList<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")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String {
|
||||||
|
return document.select("#comic_page").first().attr("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) =
|
||||||
|
network.request(get("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
||||||
|
.map { it.body().string() }
|
||||||
|
.flatMap { doLogin(it, username, password) }
|
||||||
|
.map { isAuthenticationSuccessful(it) }
|
||||||
|
|
||||||
|
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
|
||||||
|
val doc = Jsoup.parse(response)
|
||||||
|
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 network.request(post(url, headers, payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoginRequired() = true
|
||||||
|
|
||||||
|
override fun isAuthenticationSuccessful(response: Response) =
|
||||||
|
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||||
|
|
||||||
|
override fun isLogged(): Boolean {
|
||||||
|
try {
|
||||||
|
return network.cookies.get(URI(baseUrl)).find { it.name() == "pass_hash" } != null
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,195 +6,113 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.network.get
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
import eu.kanade.tachiyomi.data.network.post
|
import eu.kanade.tachiyomi.data.network.post
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.Parser
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.jsoup.Jsoup
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class Kissmanga(context: Context) : Source(context) {
|
class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
override fun getName() = NAME
|
override val name = "Kissmanga"
|
||||||
|
|
||||||
override fun getBaseUrl() = BASE_URL
|
override val baseUrl = "http://kissmanga.com"
|
||||||
|
|
||||||
override fun getLang() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
override val networkClient: OkHttpClient
|
override val client: OkHttpClient get() = network.cloudflareClient
|
||||||
get() = networkService.cloudflareClient
|
|
||||||
|
|
||||||
override fun getInitialPopularMangasUrl(): String {
|
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
|
||||||
return String.format(POPULAR_MANGAS_URL, 1)
|
|
||||||
|
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("td a:eq(0)").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInitialSearchUrl(query: String): String {
|
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||||
return SEARCH_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = getInitialSearchUrl(query)
|
page.url = searchMangaInitialUrl(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
val form = FormBody.Builder().apply {
|
||||||
form.add("authorArtist", "")
|
add("authorArtist", "")
|
||||||
form.add("mangaName", query)
|
add("mangaName", query)
|
||||||
form.add("status", "")
|
add("status", "")
|
||||||
form.add("genres", "")
|
add("genres", "")
|
||||||
|
}.build()
|
||||||
|
|
||||||
return post(page.url, requestHeaders, form.build())
|
return post(page.url, headers, form)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapterUrl: String): Request {
|
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
|
||||||
return post(baseUrl + chapterUrl, requestHeaders)
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
override fun searchMangaNextPageSelector() = null
|
||||||
return get(page.imageUrl)
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val infoElement = document.select("div.barContent").first()
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parsePopularMangasFromHtml(parsedHtml: Document): List<Manga> {
|
fun parseStatus(status: String) = when {
|
||||||
val mangaList = ArrayList<Manga>()
|
status.contains("Ongoing") -> Manga.ONGOING
|
||||||
|
status.contains("Completed") -> Manga.COMPLETED
|
||||||
for (currentHtmlBlock in parsedHtml.select("table.listing tr:gt(1)")) {
|
else -> Manga.UNKNOWN
|
||||||
val manga = constructPopularMangaFromHtml(currentHtmlBlock)
|
|
||||||
mangaList.add(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun constructPopularMangaFromHtml(htmlBlock: Element): Manga {
|
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
||||||
val manga = Manga()
|
|
||||||
manga.source = id
|
|
||||||
|
|
||||||
val urlElement = Parser.element(htmlBlock, "td a:eq(0)")
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
if (urlElement != null) {
|
chapter.setUrl(urlElement.attr("href"))
|
||||||
manga.setUrl(urlElement.attr("href"))
|
chapter.name = urlElement.text()
|
||||||
manga.title = urlElement.text()
|
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||||
}
|
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
||||||
|
} ?: 0
|
||||||
return manga
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseNextPopularMangasUrl(parsedHtml: Document, page: MangasPage): String? {
|
override fun pageListRequest(chapter: Chapter) = post(baseUrl + chapter.url, headers)
|
||||||
val path = Parser.href(parsedHtml, "li > a:contains(› Next)")
|
|
||||||
return if (path != null) BASE_URL + path else null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseSearchFromHtml(parsedHtml: Document): List<Manga> {
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
return parsePopularMangasFromHtml(parsedHtml)
|
//language=RegExp
|
||||||
}
|
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
|
||||||
|
val m = p.matcher(response.body().string())
|
||||||
override fun parseNextSearchUrl(parsedHtml: Document, page: MangasPage, query: String): String? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseHtmlToManga(mangaUrl: String, unparsedHtml: String): Manga {
|
|
||||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
|
||||||
val infoElement = parsedDocument.select("div.barContent").first()
|
|
||||||
|
|
||||||
val manga = Manga.create(mangaUrl)
|
|
||||||
manga.title = Parser.text(infoElement, "a.bigChar")
|
|
||||||
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a")
|
|
||||||
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)")
|
|
||||||
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p")
|
|
||||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))")!!)
|
|
||||||
|
|
||||||
val thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img")
|
|
||||||
if (thumbnail != null) {
|
|
||||||
manga.thumbnail_url = thumbnail
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.initialized = true
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String): Int {
|
|
||||||
if (status.contains("Ongoing")) {
|
|
||||||
return Manga.ONGOING
|
|
||||||
}
|
|
||||||
if (status.contains("Completed")) {
|
|
||||||
return Manga.COMPLETED
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseHtmlToChapters(unparsedHtml: String): List<Chapter> {
|
|
||||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
|
||||||
val chapterList = ArrayList<Chapter>()
|
|
||||||
|
|
||||||
for (chapterElement in parsedDocument.select("table.listing tr:gt(1)")) {
|
|
||||||
val chapter = constructChapterFromHtmlBlock(chapterElement)
|
|
||||||
chapterList.add(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterList
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun constructChapterFromHtmlBlock(chapterElement: Element): Chapter {
|
|
||||||
val chapter = Chapter.create()
|
|
||||||
|
|
||||||
val urlElement = Parser.element(chapterElement, "a")
|
|
||||||
val date = Parser.text(chapterElement, "td:eq(1)")
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
}
|
|
||||||
if (date != null) {
|
|
||||||
try {
|
|
||||||
chapter.date_upload = SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).time
|
|
||||||
} catch (e: ParseException) { /* Ignore */
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseHtmlToPageUrls(unparsedHtml: String): List<String> {
|
|
||||||
val parsedDocument = Jsoup.parse(unparsedHtml)
|
|
||||||
val pageUrlList = ArrayList<String>()
|
|
||||||
|
|
||||||
val numImages = parsedDocument.select("#divImage img").size
|
|
||||||
|
|
||||||
for (i in 0..numImages - 1) {
|
|
||||||
pageUrlList.add("")
|
|
||||||
}
|
|
||||||
return pageUrlList
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
|
|
||||||
val p = Pattern.compile("lstImages.push\\(\"(.+?)\"")
|
|
||||||
val m = p.matcher(unparsedHtml)
|
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
pages[i++].imageUrl = m.group(1)
|
pages.add(Page(i++, "", m.group(1)))
|
||||||
}
|
}
|
||||||
return pages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseHtmlToImageUrl(unparsedHtml: String): String? {
|
// Not used
|
||||||
return null
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
override fun imageUrlRequest(page: Page) = get(page.url)
|
||||||
|
|
||||||
val NAME = "Kissmanga"
|
override fun imageUrlParse(document: Document) = ""
|
||||||
val BASE_URL = "http://kissmanga.com"
|
|
||||||
val POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s"
|
|
||||||
val SEARCH_URL = BASE_URL + "/AdvanceSearch"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,245 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
|
|
||||||
public class Mangafox extends Source {
|
|
||||||
|
|
||||||
public static final String NAME = "Mangafox";
|
|
||||||
public static final String BASE_URL = "http://mangafox.me";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
|
||||||
public static final String SEARCH_URL =
|
|
||||||
BASE_URL + "/search.php?name_method=cw&advopts=1&order=za&sort=views&name=%s&page=%s";
|
|
||||||
|
|
||||||
public Mangafox(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getEN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return String.format(POPULAR_MANGAS_URL, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div#mangalist > ul.list > li")) {
|
|
||||||
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "a.title");
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text();
|
|
||||||
}
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
|
||||||
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("table#listing > tbody > tr:gt(0)")) {
|
|
||||||
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga mangaFromHtmlBlock = new Manga();
|
|
||||||
mangaFromHtmlBlock.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "a.series_preview");
|
|
||||||
if (urlElement != null) {
|
|
||||||
mangaFromHtmlBlock.setUrl(urlElement.attr("href"));
|
|
||||||
mangaFromHtmlBlock.title = urlElement.text();
|
|
||||||
}
|
|
||||||
return mangaFromHtmlBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
|
||||||
return next != null ? BASE_URL + next.attr("href") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
Element infoElement = parsedDocument.select("div#title").first();
|
|
||||||
Element rowElement = infoElement.select("table > tbody > tr:eq(1)").first();
|
|
||||||
Element sideInfoElement = parsedDocument.select("#series_info").first();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
manga.author = Parser.text(rowElement, "td:eq(1)");
|
|
||||||
manga.artist = Parser.text(rowElement, "td:eq(2)");
|
|
||||||
manga.description = Parser.text(infoElement, "p.summary");
|
|
||||||
manga.genre = Parser.text(rowElement, "td:eq(3)");
|
|
||||||
manga.thumbnail_url = Parser.src(sideInfoElement, "div.cover > img");
|
|
||||||
manga.status = parseStatus(Parser.text(sideInfoElement, ".data"));
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("Ongoing")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
}
|
|
||||||
if (status.contains("Completed")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.select("div#chapters li div")) {
|
|
||||||
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(currentChapter);
|
|
||||||
}
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = chapterElement.select("a.tips").first();
|
|
||||||
Element dateElement = chapterElement.select("span.date").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href"));
|
|
||||||
chapter.name = urlElement.text();
|
|
||||||
}
|
|
||||||
if (dateElement != null) {
|
|
||||||
chapter.date_upload = parseUpdateFromElement(dateElement);
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseUpdateFromElement(Element updateElement) {
|
|
||||||
String updatedDateAsString = updateElement.text();
|
|
||||||
|
|
||||||
if (updatedDateAsString.contains("Today")) {
|
|
||||||
Calendar today = Calendar.getInstance();
|
|
||||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
today.set(Calendar.MINUTE, 0);
|
|
||||||
today.set(Calendar.SECOND, 0);
|
|
||||||
today.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
|
||||||
return today.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return today.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else if (updatedDateAsString.contains("Yesterday")) {
|
|
||||||
Calendar yesterday = Calendar.getInstance();
|
|
||||||
yesterday.add(Calendar.DATE, -1);
|
|
||||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
yesterday.set(Calendar.MINUTE, 0);
|
|
||||||
yesterday.set(Calendar.SECOND, 0);
|
|
||||||
yesterday.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
|
||||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return yesterday.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Date specificDate = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(updatedDateAsString);
|
|
||||||
|
|
||||||
return specificDate.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
List<String> pageUrlList = new ArrayList<>();
|
|
||||||
|
|
||||||
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
|
|
||||||
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
|
|
||||||
for (Element pageUrlElement : pageUrlElements) {
|
|
||||||
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageUrlList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
Element imageElement = parsedDocument.getElementById("image");
|
|
||||||
return imageElement.attr("src");
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,122 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Mangafox"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mangafox.me"
|
||||||
|
|
||||||
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("a.title").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) =
|
||||||
|
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("a.series_preview").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val infoElement = document.select("div#title").first()
|
||||||
|
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||||
|
val sideInfoElement = document.select("#series_info").first()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> Manga.ONGOING
|
||||||
|
status.contains("Completed") -> Manga.COMPLETED
|
||||||
|
else -> Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div#chapters li div"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a.tips").first()
|
||||||
|
|
||||||
|
chapter.setUrl(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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(response: Response, pages: MutableList<Page>) {
|
||||||
|
val document = Jsoup.parse(response.body().string())
|
||||||
|
|
||||||
|
val url = response.request().url().toString().substringBeforeLast('/')
|
||||||
|
document.select("select.m").first().select("option:not([value=0])").forEach {
|
||||||
|
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||||
|
}
|
||||||
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used, overrides parent.
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
}
|
@ -1,313 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
|
|
||||||
public class Mangahere extends Source {
|
|
||||||
|
|
||||||
public static final String NAME = "Mangahere";
|
|
||||||
public static final String BASE_URL = "http://www.mangahere.co";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
|
|
||||||
|
|
||||||
public Mangahere(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getEN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return String.format(POPULAR_MANGAS_URL, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div.directory_list > ul > li")) {
|
|
||||||
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "div.title > a");
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.attr("title");
|
|
||||||
}
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
|
||||||
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
Elements mangaHtmlBlocks = parsedHtml.select("div.result_search > dl");
|
|
||||||
for (Element currentHtmlBlock : mangaHtmlBlocks) {
|
|
||||||
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "a.manga_info");
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text();
|
|
||||||
}
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
|
||||||
return next != null ? BASE_URL + next.attr("href") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseUpdateFromElement(Element updateElement) {
|
|
||||||
String updatedDateAsString = updateElement.text();
|
|
||||||
|
|
||||||
if (updatedDateAsString.contains("Today")) {
|
|
||||||
Calendar today = Calendar.getInstance();
|
|
||||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
today.set(Calendar.MINUTE, 0);
|
|
||||||
today.set(Calendar.SECOND, 0);
|
|
||||||
today.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
|
||||||
return today.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return today.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else if (updatedDateAsString.contains("Yesterday")) {
|
|
||||||
Calendar yesterday = Calendar.getInstance();
|
|
||||||
yesterday.add(Calendar.DATE, -1);
|
|
||||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
yesterday.set(Calendar.MINUTE, 0);
|
|
||||||
yesterday.set(Calendar.SECOND, 0);
|
|
||||||
yesterday.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
|
||||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return yesterday.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Date specificDate = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString);
|
|
||||||
|
|
||||||
return specificDate.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<ul class=\"detail_topText\">");
|
|
||||||
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
Element detailElement = parsedDocument.select("ul.detail_topText").first();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
manga.author = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/author/]");
|
|
||||||
manga.artist = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/artist/]");
|
|
||||||
|
|
||||||
String description = Parser.text(detailElement, "#show");
|
|
||||||
if (description != null) {
|
|
||||||
manga.description = description.substring(0, description.length() - "Show less".length());
|
|
||||||
}
|
|
||||||
String genres = Parser.text(detailElement, "li:eq(3)");
|
|
||||||
if (genres != null) {
|
|
||||||
manga.genre = genres.substring("Genre(s):".length());
|
|
||||||
}
|
|
||||||
manga.status = parseStatus(Parser.text(detailElement, "li:eq(6)"));
|
|
||||||
|
|
||||||
beginIndex = unparsedHtml.indexOf("<img");
|
|
||||||
endIndex = unparsedHtml.indexOf("/>", beginIndex);
|
|
||||||
trimmedHtml = unparsedHtml.substring(beginIndex, endIndex + 2);
|
|
||||||
|
|
||||||
parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
manga.thumbnail_url = Parser.src(parsedDocument, "img");
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("Ongoing")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
}
|
|
||||||
if (status.contains("Completed")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<ul>");
|
|
||||||
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.getElementsByTag("li")) {
|
|
||||||
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(currentChapter);
|
|
||||||
}
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = chapterElement.select("a").first();
|
|
||||||
Element dateElement = chapterElement.select("span.right").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href"));
|
|
||||||
chapter.name = urlElement.text();
|
|
||||||
}
|
|
||||||
if (dateElement != null) {
|
|
||||||
chapter.date_upload = parseDateFromElement(dateElement);
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseDateFromElement(Element dateElement) {
|
|
||||||
String dateAsString = dateElement.text();
|
|
||||||
|
|
||||||
if (dateAsString.contains("Today")) {
|
|
||||||
Calendar today = Calendar.getInstance();
|
|
||||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
today.set(Calendar.MINUTE, 0);
|
|
||||||
today.set(Calendar.SECOND, 0);
|
|
||||||
today.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Today", ""));
|
|
||||||
return today.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return today.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else if (dateAsString.contains("Yesterday")) {
|
|
||||||
Calendar yesterday = Calendar.getInstance();
|
|
||||||
yesterday.add(Calendar.DATE, -1);
|
|
||||||
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
|
||||||
yesterday.set(Calendar.MINUTE, 0);
|
|
||||||
yesterday.set(Calendar.SECOND, 0);
|
|
||||||
yesterday.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Yesterday", ""));
|
|
||||||
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return yesterday.getTimeInMillis();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Date date = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString);
|
|
||||||
|
|
||||||
return date.getTime();
|
|
||||||
} catch (ParseException e) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<div class=\"go_page clearfix\">");
|
|
||||||
int endIndex = unparsedHtml.indexOf("</div>", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
List<String> pageUrlList = new ArrayList<>();
|
|
||||||
|
|
||||||
Elements pageUrlElements = parsedDocument.select("select.wid60").first().getElementsByTag("option");
|
|
||||||
for (Element pageUrlElement : pageUrlElements) {
|
|
||||||
pageUrlList.add(pageUrlElement.attr("value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageUrlList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<section class=\"read_img\" id=\"viewer\">");
|
|
||||||
int endIndex = unparsedHtml.indexOf("</section>", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
Element imageElement = parsedDocument.getElementById("image");
|
|
||||||
|
|
||||||
return imageElement.attr("src");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,113 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Mangahere"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.mangahere.co"
|
||||||
|
|
||||||
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("div.title > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) =
|
||||||
|
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.result_search > dl"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("a.manga_info").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val detailElement = document.select(".manga_detail_top").first()
|
||||||
|
val infoElement = detailElement.select(".detail_topText").first()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> Manga.ONGOING
|
||||||
|
status.contains("Completed") -> Manga.COMPLETED
|
||||||
|
else -> Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrl(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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, pages: MutableList<Page>) {
|
||||||
|
document.select("select.wid60").first().getElementsByTag("option").forEach {
|
||||||
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
}
|
||||||
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
}
|
@ -1,290 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
import okhttp3.Headers;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.functions.Action1;
|
|
||||||
import rx.functions.Func1;
|
|
||||||
|
|
||||||
public class ReadMangaToday extends Source {
|
|
||||||
public static final String NAME = "ReadMangaToday";
|
|
||||||
public static final String BASE_URL = "http://www.readmanga.today";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/hot-manga/%s";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/service/search?q=%s";
|
|
||||||
|
|
||||||
private static JsonParser parser = new JsonParser();
|
|
||||||
private static Gson gson = new Gson();
|
|
||||||
|
|
||||||
public ReadMangaToday(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return String.format(POPULAR_MANGAS_URL, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getEN();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div.hot-manga > div.style-list > div.box")) {
|
|
||||||
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(htmlBlock, "div.title > h2 > a");
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.attr("title");
|
|
||||||
}
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
Element next = Parser.element(parsedHtml, "div.hot-manga > ul.pagination > li > a:contains(»)");
|
|
||||||
return next != null ? next.attr("href") : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
|
|
||||||
return networkService
|
|
||||||
.requestBody(searchMangaRequest(page, query), networkService.getDefaultClient())
|
|
||||||
.doOnNext(new Action1<String>() {
|
|
||||||
@Override
|
|
||||||
public void call(String doc) {
|
|
||||||
page.mangas = ReadMangaToday.this.parseSearchFromJson(doc);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(new Func1<String, MangasPage>() {
|
|
||||||
@Override
|
|
||||||
public MangasPage call(String response) {
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Headers.Builder headersBuilder() {
|
|
||||||
return super.headersBuilder().add("X-Requested-With", "XMLHttpRequest");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<Manga> parseSearchFromJson(String unparsedJson) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
JsonArray mangasArray = parser.parse(unparsedJson).getAsJsonArray();
|
|
||||||
|
|
||||||
for (JsonElement mangaElement : mangasArray) {
|
|
||||||
Manga currentManga = constructSearchMangaFromJsonObject(mangaElement.getAsJsonObject());
|
|
||||||
mangaList.add(currentManga);
|
|
||||||
}
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructSearchMangaFromJsonObject(JsonObject jsonObject) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
manga.setUrl(gson.fromJson(jsonObject.get("url"), String.class));
|
|
||||||
manga.title = gson.fromJson(jsonObject.get("title"), String.class);
|
|
||||||
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
|
||||||
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
Element detailElement = parsedDocument.select("div.movie-meta").first();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
for (Element castHtmlBlock : parsedDocument.select("div.cast ul.cast-list > li")) {
|
|
||||||
String name = Parser.text(castHtmlBlock, "ul > li > a");
|
|
||||||
String role = Parser.text(castHtmlBlock, "ul > li:eq(1)");
|
|
||||||
if (role.equals("Author")) {
|
|
||||||
manga.author = name;
|
|
||||||
} else if (role.equals("Artist")) {
|
|
||||||
manga.artist = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String description = Parser.text(detailElement, "li.movie-detail");
|
|
||||||
if (description != null) {
|
|
||||||
manga.description = description;
|
|
||||||
}
|
|
||||||
String genres = Parser.text(detailElement, "dl.dl-horizontal > dd:eq(5)");
|
|
||||||
if (genres != null) {
|
|
||||||
manga.genre = genres;
|
|
||||||
}
|
|
||||||
manga.status = parseStatus(Parser.text(detailElement, "dl.dl-horizontal > dd:eq(3)"));
|
|
||||||
manga.thumbnail_url = Parser.src(detailElement, "img.img-responsive");
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("Ongoing")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
} else if (status.contains("Completed")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
|
||||||
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.select("ul.chp_lst > li")) {
|
|
||||||
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(currentChapter);
|
|
||||||
}
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = chapterElement.select("a").first();
|
|
||||||
Element dateElement = chapterElement.select("span.dte").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href"));
|
|
||||||
chapter.name = urlElement.select("span.val").text();
|
|
||||||
}
|
|
||||||
if (dateElement != null) {
|
|
||||||
chapter.date_upload = parseDateFromElement(dateElement);
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseDateFromElement(Element dateElement) {
|
|
||||||
String dateAsString = dateElement.text();
|
|
||||||
String[] dateWords = dateAsString.split(" ");
|
|
||||||
|
|
||||||
if (dateWords.length == 3) {
|
|
||||||
int timeAgo = Integer.parseInt(dateWords[0]);
|
|
||||||
Calendar date = 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.getTimeInMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
|
||||||
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
List<String> pageUrlList = new ArrayList<>();
|
|
||||||
|
|
||||||
Elements pageUrlElements = parsedDocument.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option");
|
|
||||||
for (Element pageUrlElement : pageUrlElements) {
|
|
||||||
pageUrlList.add(pageUrlElement.attr("value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageUrlList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
|
||||||
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
|
||||||
|
|
||||||
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
|
||||||
|
|
||||||
Element imageElement = Parser.element(parsedDocument, "img.img-responsive-2");
|
|
||||||
|
|
||||||
return imageElement.attr("src");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,127 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.network.post
|
||||||
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "ReadMangaToday"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.readmanga.today"
|
||||||
|
|
||||||
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("div.title > h2 > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) =
|
||||||
|
"$baseUrl/search"
|
||||||
|
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = searchMangaInitialUrl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = okhttp3.FormBody.Builder()
|
||||||
|
builder.add("query", query)
|
||||||
|
|
||||||
|
return post(page.url, headers, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.content-list > div.style-list > div.box"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("div.title > h2 > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val detailElement = document.select("div.movie-meta").first()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> Manga.ONGOING
|
||||||
|
status.contains("Completed") -> Manga.COMPLETED
|
||||||
|
else -> Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul.chp_lst > li"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrl(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.select("span.val").text()
|
||||||
|
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
val dateWords : List<String> = date.split(" ")
|
||||||
|
|
||||||
|
if (dateWords.size == 3) {
|
||||||
|
val timeAgo = Integer.parseInt(dateWords[0])
|
||||||
|
var 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.getTimeInMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||||
|
|
||||||
|
}
|
@ -1,240 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
|
|
||||||
public class Mangachan extends Source {
|
|
||||||
|
|
||||||
public static final String NAME = "Mangachan";
|
|
||||||
public static final String BASE_URL = "http://mangachan.ru";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/mostfavorites";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/?do=search&subaction=search&story=%s";
|
|
||||||
|
|
||||||
public Mangachan(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getRU();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return POPULAR_MANGAS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div.content_row")) {
|
|
||||||
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
|
||||||
mangaList.add(manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = currentHtmlBlock.getElementsByTag("h2").select("a").first();
|
|
||||||
Element imgElement = currentHtmlBlock.getElementsByClass("manga_images").select("img").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imgElement != null) {
|
|
||||||
manga.thumbnail_url = BASE_URL + imgElement.attr("src");
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
String path = Parser.href(parsedHtml, "a:contains(Вперед)");
|
|
||||||
return path != null ? POPULAR_MANGAS_URL + path : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
return parsePopularMangasFromHtml(parsedHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
|
|
||||||
Element infoElement = parsedDocument.getElementsByClass("mangatitle").first();
|
|
||||||
String description = parsedDocument.getElementById("description").text();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
|
|
||||||
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 = description.replaceAll("Прислать описание", "");
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("перевод продолжается")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
} else if (status.contains("перевод завершен")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
} else return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.select("table.table_cha tr:gt(1)")) {
|
|
||||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(chapter);
|
|
||||||
}
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = chapterElement.select("a").first();
|
|
||||||
String date = Parser.text(chapterElement, "div.date");
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.name = urlElement.text();
|
|
||||||
chapter.url = urlElement.attr("href");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date != null) {
|
|
||||||
try {
|
|
||||||
chapter.date_upload = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date).getTime();
|
|
||||||
} catch (ParseException e) { /* Ignore */ }
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Without this extra chapters are in the wrong place in the list
|
|
||||||
@Override
|
|
||||||
public void parseChapterNumber(Chapter chapter) {
|
|
||||||
// For chapters with url like /online/254903-fairy-tail_v56_ch474.html
|
|
||||||
String url = chapter.url.replace(".html", "");
|
|
||||||
Pattern pattern = Pattern.compile("\\d+_ch[\\d.]+");
|
|
||||||
Matcher matcher = pattern.matcher(url);
|
|
||||||
|
|
||||||
if (matcher.find()) {
|
|
||||||
String[] parts = matcher.group().split("_ch");
|
|
||||||
chapter.chapter_number = Float.parseFloat(parts[0] + "." + AddZero(parts[1]));
|
|
||||||
} else { // For chapters with url like /online/61216-3298.html
|
|
||||||
String name = chapter.name;
|
|
||||||
name = name.replaceAll("[\\s\\d\\w\\W]+v", "");
|
|
||||||
String volume = name.substring(0, name.indexOf(" - "));
|
|
||||||
String[] parts = name.replaceFirst("\\d+ - ", "").split(" ");
|
|
||||||
|
|
||||||
chapter.chapter_number = Float.parseFloat(volume + "." + AddZero(parts[0]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String AddZero(String num) {
|
|
||||||
if (Float.parseFloat(num) < 1000f) {
|
|
||||||
num = "0" + num.replace(".", "");
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(num) < 100f) {
|
|
||||||
num = "0" + num.replace(".", "");
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(num) < 10f) {
|
|
||||||
num = "0" + num.replace(".", "");
|
|
||||||
}
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
ArrayList<String> pages = new ArrayList<>();
|
|
||||||
|
|
||||||
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
|
|
||||||
int endIndex = unparsedHtml.indexOf(']', beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("\"", "");
|
|
||||||
|
|
||||||
String[] pageUrls = trimmedHtml.split(",");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
pages.add("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
|
|
||||||
int endIndex = unparsedHtml.indexOf(']', beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("\"", "");
|
|
||||||
|
|
||||||
String[] pageUrls = trimmedHtml.split(",");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
pages.get(i).setImageUrl(pageUrls[i].replaceAll("im.?\\.", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (List<Page>) pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,95 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.RU
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Mangachan"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mangachan.ru"
|
||||||
|
|
||||||
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.content_row"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("h2 > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
element.select("img").first().let {
|
||||||
|
manga.thumbnail_url = baseUrl + it.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val infoElement = document.select("table.mangatitle").first()
|
||||||
|
val descElement = document.select("div#description").first()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("перевод завершен") -> return Manga.COMPLETED
|
||||||
|
element.contains("перевод продолжается") -> return Manga.ONGOING
|
||||||
|
else -> return Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrl(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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<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(',')
|
||||||
|
|
||||||
|
for ((i, url) in pageUrls.withIndex()) {
|
||||||
|
pages.add(Page(i, "", url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
}
|
@ -1,225 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
|
|
||||||
public class Mintmanga extends Source {
|
|
||||||
|
|
||||||
public static final String NAME = "Mintmanga";
|
|
||||||
public static final String BASE_URL = "http://mintmanga.com";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
|
|
||||||
|
|
||||||
public Mintmanga(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getRU();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return POPULAR_MANGAS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
|
|
||||||
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
|
||||||
mangaList.add(manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
String path = Parser.href(parsedHtml, "a:contains(→)");
|
|
||||||
return path != null ? BASE_URL + path : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
return parsePopularMangasFromHtml(parsedHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
Element infoElement = parsedDocument.select("div.leftContent").first();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
manga.title = Parser.text(infoElement, "span.eng-name");
|
|
||||||
manga.author = Parser.text(infoElement, "span.elem_author ");
|
|
||||||
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
|
|
||||||
manga.description = Parser.allText(infoElement, "div.manga-description");
|
|
||||||
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
|
|
||||||
manga.status = Manga.COMPLETED;
|
|
||||||
} else {
|
|
||||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
|
|
||||||
}
|
|
||||||
|
|
||||||
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
|
|
||||||
if (thumbnail != null) {
|
|
||||||
manga.thumbnail_url = thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("продолжается")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
}
|
|
||||||
if (status.contains("завершен")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
|
|
||||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(chapterElement, "a");
|
|
||||||
String date = Parser.text(chapterElement, "td:eq(1)");
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1");
|
|
||||||
chapter.name = urlElement.text().replaceAll(" новое", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date != null) {
|
|
||||||
try {
|
|
||||||
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
|
|
||||||
} catch (ParseException e) { /* Ignore */ }
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Without this extra chapters are in the wrong place in the list
|
|
||||||
@Override
|
|
||||||
public void parseChapterNumber(Chapter chapter) {
|
|
||||||
String url = chapter.url.replace("?mature=1", "");
|
|
||||||
|
|
||||||
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 1000f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 100f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 10f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
ArrayList<String> pages = new ArrayList<>();
|
|
||||||
|
|
||||||
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
|
||||||
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
|
||||||
String[] pageUrls = trimmedHtml.split("],\\[");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
pages.add("");
|
|
||||||
}
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
|
||||||
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
|
||||||
String[] pageUrls = trimmedHtml.split("],\\[");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
String[] urlParts = pageUrls[i].split(","); // auto/06/35,http://e4.adultmanga.me/,/55/01.png
|
|
||||||
String page = urlParts[1] + urlParts[0] + urlParts[2];
|
|
||||||
pages.get(i).setImageUrl(page);
|
|
||||||
}
|
|
||||||
return (List<Page>) pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,102 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.RU
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
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(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Mintmanga"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mintmanga.com"
|
||||||
|
|
||||||
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("h3 > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val infoElement = document.select("div.leftContent").first()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
|
||||||
|
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
|
||||||
|
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
|
||||||
|
else -> return Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrl(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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseChapterNumber(chapter: Chapter) {
|
||||||
|
chapter.chapter_number = -2f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<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).replace("[\"\']+".toRegex(), "")
|
||||||
|
|
||||||
|
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
||||||
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (m.find()) {
|
||||||
|
val urlParts = m.group().split(',')
|
||||||
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
}
|
@ -1,225 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
|
||||||
import org.jsoup.nodes.Document;
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language;
|
|
||||||
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.Parser;
|
|
||||||
|
|
||||||
public class Readmanga extends Source {
|
|
||||||
|
|
||||||
public static final String NAME = "Readmanga";
|
|
||||||
public static final String BASE_URL = "http://readmanga.me";
|
|
||||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
|
|
||||||
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
|
|
||||||
|
|
||||||
public Readmanga(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Language getLang() {
|
|
||||||
return LanguageKt.getRU();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialPopularMangasUrl() {
|
|
||||||
return POPULAR_MANGAS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getInitialSearchUrl(String query) {
|
|
||||||
return String.format(SEARCH_URL, Uri.encode(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
|
||||||
List<Manga> mangaList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
|
|
||||||
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
|
||||||
mangaList.add(manga);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
|
||||||
Manga manga = new Manga();
|
|
||||||
manga.source = getId();
|
|
||||||
|
|
||||||
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
manga.setUrl(urlElement.attr("href"));
|
|
||||||
manga.title = urlElement.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
|
||||||
String path = Parser.href(parsedHtml, "a:contains(→)");
|
|
||||||
return path != null ? BASE_URL + path : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
|
||||||
return parsePopularMangasFromHtml(parsedHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
Element infoElement = parsedDocument.select("div.leftContent").first();
|
|
||||||
|
|
||||||
Manga manga = Manga.create(mangaUrl);
|
|
||||||
manga.title = Parser.text(infoElement, "span.eng-name");
|
|
||||||
manga.author = Parser.text(infoElement, "span.elem_author ");
|
|
||||||
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
|
|
||||||
manga.description = Parser.allText(infoElement, "div.manga-description");
|
|
||||||
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
|
|
||||||
manga.status = Manga.COMPLETED;
|
|
||||||
} else {
|
|
||||||
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
|
|
||||||
}
|
|
||||||
|
|
||||||
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
|
|
||||||
if (thumbnail != null) {
|
|
||||||
manga.thumbnail_url = thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.initialized = true;
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseStatus(String status) {
|
|
||||||
if (status.contains("продолжается")) {
|
|
||||||
return Manga.ONGOING;
|
|
||||||
}
|
|
||||||
if (status.contains("завершен")) {
|
|
||||||
return Manga.COMPLETED;
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
|
||||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
|
||||||
List<Chapter> chapterList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
|
|
||||||
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
|
||||||
chapterList.add(chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
|
||||||
Chapter chapter = Chapter.create();
|
|
||||||
|
|
||||||
Element urlElement = Parser.element(chapterElement, "a");
|
|
||||||
String date = Parser.text(chapterElement, "td:eq(1)");
|
|
||||||
|
|
||||||
if (urlElement != null) {
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1");
|
|
||||||
chapter.name = urlElement.text().replaceAll(" новое", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date != null) {
|
|
||||||
try {
|
|
||||||
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
|
|
||||||
} catch (ParseException e) { /* Ignore */ }
|
|
||||||
}
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Without this extra chapters are in the wrong place in the list
|
|
||||||
@Override
|
|
||||||
public void parseChapterNumber(Chapter chapter) {
|
|
||||||
String url = chapter.url.replace("?mature=1", "");
|
|
||||||
|
|
||||||
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 1000f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 100f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
if (Float.parseFloat(urlParts[1]) < 10f) {
|
|
||||||
urlParts[1] = "0" + urlParts[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
|
||||||
ArrayList<String> pages = new ArrayList<>();
|
|
||||||
|
|
||||||
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
|
||||||
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
|
||||||
String[] pageUrls = trimmedHtml.split("],\\[");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
pages.add("");
|
|
||||||
}
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
|
||||||
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
|
||||||
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
|
||||||
|
|
||||||
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
|
||||||
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
|
||||||
String[] pageUrls = trimmedHtml.split("],\\[");
|
|
||||||
for (int i = 0; i < pageUrls.length; i++) {
|
|
||||||
String[] urlParts = pageUrls[i].split(","); // auto/12/56,http://e7.postfact.ru/,/51/01.jpg_res.jpg
|
|
||||||
String page = urlParts[1] + urlParts[0] + urlParts[2];
|
|
||||||
pages.get(i).setImageUrl(page);
|
|
||||||
}
|
|
||||||
return (List<Page>) pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,102 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.RU
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
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(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
||||||
|
|
||||||
|
override val name = "Readmanga"
|
||||||
|
|
||||||
|
override val baseUrl = "http://readmanga.me"
|
||||||
|
|
||||||
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("h3 > a").first().let {
|
||||||
|
manga.setUrl(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val infoElement = document.select("div.leftContent").first()
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
|
||||||
|
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
|
||||||
|
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
|
||||||
|
else -> return Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrl(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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseChapterNumber(chapter: Chapter) {
|
||||||
|
chapter.chapter_number = -2f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<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).replace("[\"\']+".toRegex(), "")
|
||||||
|
|
||||||
|
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
||||||
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (m.find()) {
|
||||||
|
val urlParts = m.group().split(',')
|
||||||
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
}
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.glide.MangaModelLoader
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
|
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
|
||||||
import eu.kanade.tachiyomi.injection.module.AppModule
|
import eu.kanade.tachiyomi.injection.module.AppModule
|
||||||
@ -17,7 +18,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
|||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
|
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
|
||||||
@ -43,12 +44,14 @@ interface AppComponent {
|
|||||||
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
|
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
|
||||||
fun inject(backupPresenter: BackupPresenter)
|
fun inject(backupPresenter: BackupPresenter)
|
||||||
|
|
||||||
fun inject(mangaActivity: MangaActivity)
|
fun inject(mainActivity: MainActivity)
|
||||||
fun inject(settingsActivity: SettingsActivity)
|
fun inject(settingsActivity: SettingsActivity)
|
||||||
|
|
||||||
fun inject(source: Source)
|
fun inject(source: Source)
|
||||||
fun inject(mangaSyncService: MangaSyncService)
|
fun inject(mangaSyncService: MangaSyncService)
|
||||||
|
|
||||||
|
fun inject(onlineSource: OnlineSource)
|
||||||
|
|
||||||
fun inject(libraryUpdateService: LibraryUpdateService)
|
fun inject(libraryUpdateService: LibraryUpdateService)
|
||||||
fun inject(downloadService: DownloadService)
|
fun inject(downloadService: DownloadService)
|
||||||
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
||||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
@ -52,7 +53,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Active source.
|
* Active source.
|
||||||
*/
|
*/
|
||||||
lateinit var source: Source
|
lateinit var source: OnlineSource
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,7 +164,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
*
|
*
|
||||||
* @param source the new active source.
|
* @param source the new active source.
|
||||||
*/
|
*/
|
||||||
fun setActiveSource(source: Source) {
|
fun setActiveSource(source: OnlineSource) {
|
||||||
prefs.lastUsedCatalogueSource().set(source.id)
|
prefs.lastUsedCatalogueSource().set(source.id)
|
||||||
this.source = source
|
this.source = source
|
||||||
restartPager()
|
restartPager()
|
||||||
@ -222,9 +223,9 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val observable = if (query.isEmpty())
|
val observable = if (query.isEmpty())
|
||||||
source.pullPopularMangasFromNetwork(nextMangasPage)
|
source.fetchPopularManga(nextMangasPage)
|
||||||
else
|
else
|
||||||
source.searchMangasFromNetwork(nextMangasPage, query)
|
source.fetchSearchManga(nextMangasPage, query)
|
||||||
|
|
||||||
return observable.subscribeOn(Schedulers.io())
|
return observable.subscribeOn(Schedulers.io())
|
||||||
.doOnNext { lastMangasPage = it }
|
.doOnNext { lastMangasPage = it }
|
||||||
@ -268,7 +269,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
* @return an observable of the manga to initialize
|
* @return an observable of the manga to initialize
|
||||||
*/
|
*/
|
||||||
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
||||||
return source.pullMangaFromNetwork(manga.url)
|
return source.fetchMangaDetails(manga)
|
||||||
.flatMap { networkManga ->
|
.flatMap { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
@ -282,13 +283,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
*
|
*
|
||||||
* @return a source.
|
* @return a source.
|
||||||
*/
|
*/
|
||||||
fun getLastUsedSource(): Source {
|
fun getLastUsedSource(): OnlineSource {
|
||||||
val id = prefs.lastUsedCatalogueSource().get() ?: -1
|
val id = prefs.lastUsedCatalogueSource().get() ?: -1
|
||||||
val source = sourceManager.get(id)
|
val source = sourceManager.get(id)
|
||||||
if (!isValidSource(source)) {
|
if (!isValidSource(source)) {
|
||||||
return findFirstValidSource()
|
return findFirstValidSource()
|
||||||
}
|
}
|
||||||
return source!!
|
return source as OnlineSource
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,10 +299,10 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
* @return true if the source is valid, false otherwise.
|
* @return true if the source is valid, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isValidSource(source: Source?): Boolean {
|
fun isValidSource(source: Source?): Boolean {
|
||||||
if (source == null) return false
|
if (source == null || source !is OnlineSource) return false
|
||||||
|
|
||||||
return with(source) {
|
return with(source) {
|
||||||
if (!isLoginRequired || isLogged) {
|
if (!isLoginRequired() || isLogged()) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
prefs.sourceUsername(this) != "" && prefs.sourcePassword(this) != ""
|
prefs.sourceUsername(this) != "" && prefs.sourcePassword(this) != ""
|
||||||
@ -314,14 +315,14 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
*
|
*
|
||||||
* @return the index of the first valid source.
|
* @return the index of the first valid source.
|
||||||
*/
|
*/
|
||||||
fun findFirstValidSource(): Source {
|
fun findFirstValidSource(): OnlineSource {
|
||||||
return sources.find { isValidSource(it) }!!
|
return sources.first { isValidSource(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of enabled sources ordered by language and name.
|
* Returns a list of enabled sources ordered by language and name.
|
||||||
*/
|
*/
|
||||||
private fun getEnabledSources(): List<Source> {
|
private fun getEnabledSources(): List<OnlineSource> {
|
||||||
val languages = prefs.enabledLanguages().getOrDefault()
|
val languages = prefs.enabledLanguages().getOrDefault()
|
||||||
|
|
||||||
// Ensure at least one language
|
// Ensure at least one language
|
||||||
@ -329,7 +330,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
languages.add(EN.code)
|
languages.add(EN.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceManager.getSources()
|
return sourceManager.getOnlineSources()
|
||||||
.filter { it.lang.code in languages }
|
.filter { it.lang.code in languages }
|
||||||
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
.sortedBy { "(${it.lang.code}) ${it.name}" }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.main
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v4.app.FragmentManager
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
|
||||||
|
|
||||||
|
class ChangelogDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun show(preferences: PreferencesHelper, fragmentManager: FragmentManager) {
|
||||||
|
if (preferences.lastVersionCode().getOrDefault() < BuildConfig.VERSION_CODE) {
|
||||||
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
ChangelogDialogFragment().show(fragmentManager, "changelog")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||||
|
val view = WhatsNewRecyclerView(context)
|
||||||
|
return MaterialDialog.Builder(activity)
|
||||||
|
.title("Changelog")
|
||||||
|
.customView(view, false)
|
||||||
|
.positiveText(android.R.string.yes)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
class WhatsNewRecyclerView(context: Context) : ChangeLogRecyclerView(context) {
|
||||||
|
override fun initAttrs(attrs: AttributeSet?, defStyle: Int) {
|
||||||
|
mRowLayoutId = R.layout.changelog_row_layout
|
||||||
|
mRowHeaderLayoutId = R.layout.changelog_header_layout
|
||||||
|
mChangeLogFileResourceId = if (BuildConfig.DEBUG) R.raw.changelog_debug else R.raw.changelog_release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,9 @@ import android.os.Bundle
|
|||||||
import android.support.v4.app.Fragment
|
import android.support.v4.app.Fragment
|
||||||
import android.support.v4.view.GravityCompat
|
import android.support.v4.view.GravityCompat
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
||||||
@ -15,9 +17,12 @@ import eu.kanade.tachiyomi.ui.recent.RecentChaptersFragment
|
|||||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
import kotlinx.android.synthetic.main.toolbar.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MainActivity : BaseActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var preferences: PreferencesHelper
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
setAppTheme()
|
setAppTheme()
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
@ -28,6 +33,8 @@ class MainActivity : BaseActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
App.get(this).component.inject(this)
|
||||||
|
|
||||||
// Inflate activity_main.xml.
|
// Inflate activity_main.xml.
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
@ -54,6 +61,7 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
setFragment(LibraryFragment.newInstance())
|
setFragment(LibraryFragment.newInstance())
|
||||||
|
ChangelogDialogFragment.show(preferences, supportFragmentManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,16 +214,16 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
private fun showSortingDialog() {
|
private fun showSortingDialog() {
|
||||||
// Get available modes, ids and the selected mode
|
// Get available modes, ids and the selected mode
|
||||||
val modes = intArrayOf(R.string.sort_by_number, R.string.sort_by_source)
|
val modes = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
|
||||||
val ids = intArrayOf(Manga.SORTING_NUMBER, Manga.SORTING_SOURCE)
|
val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
|
||||||
val selectedIndex = if (presenter.manga.sorting == Manga.SORTING_NUMBER) 0 else 1
|
val selectedIndex = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
MaterialDialog.Builder(activity)
|
||||||
.title(R.string.sorting_mode)
|
.title(R.string.sorting_mode)
|
||||||
.items(modes.map { getString(it) })
|
.items(modes.map { getString(it) })
|
||||||
.itemsIds(ids)
|
.itemsIds(ids)
|
||||||
.itemsCallbackSingleChoice(selectedIndex) { dialog, itemView, which, text ->
|
.itemsCallbackSingleChoice(selectedIndex) { dialog, itemView, which, text ->
|
||||||
// Save the new display mode
|
// Save the new sorting mode
|
||||||
presenter.setSorting(itemView.id)
|
presenter.setSorting(itemView.id)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -232,13 +232,13 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
|||||||
|
|
||||||
private fun showDownloadDialog() {
|
private fun showDownloadDialog() {
|
||||||
// Get available modes
|
// Get available modes
|
||||||
val modes = listOf(getString(R.string.download_1), getString(R.string.download_5), getString(R.string.download_10),
|
val modes = intArrayOf(R.string.download_1, R.string.download_5, R.string.download_10,
|
||||||
getString(R.string.download_unread), getString(R.string.download_all))
|
R.string.download_unread, R.string.download_all)
|
||||||
|
|
||||||
MaterialDialog.Builder(activity)
|
MaterialDialog.Builder(activity)
|
||||||
.title(R.string.manga_download)
|
.title(R.string.manga_download)
|
||||||
.negativeText(android.R.string.cancel)
|
.negativeText(android.R.string.cancel)
|
||||||
.items(modes)
|
.items(modes.map { getString(it) })
|
||||||
.itemsCallback { dialog, view, i, charSequence ->
|
.itemsCallback { dialog, view, i, charSequence ->
|
||||||
var chapters: MutableList<Chapter> = arrayListOf()
|
var chapters: MutableList<Chapter> = arrayListOf()
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getOnlineChaptersObs(): Observable<Pair<Int, Int>> {
|
fun getOnlineChaptersObs(): Observable<Pair<Int, Int>> {
|
||||||
return source.pullChaptersFromNetwork(manga.url)
|
return source.fetchChapterList(manga)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -8,6 +8,7 @@ import com.bumptech.glide.Glide
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
import eu.kanade.tachiyomi.util.getResourceColor
|
import eu.kanade.tachiyomi.util.getResourceColor
|
||||||
@ -96,7 +97,7 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
|
|
||||||
// If manga source is known update source TextView.
|
// If manga source is known update source TextView.
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
manga_source.text = source.visibleName
|
manga_source.text = source.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update genres TextView.
|
// Update genres TextView.
|
||||||
@ -140,8 +141,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
|||||||
* Open the manga in browser.
|
* Open the manga in browser.
|
||||||
*/
|
*/
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
|
val source = presenter.source as? OnlineSource ?: return
|
||||||
try {
|
try {
|
||||||
val url = Uri.parse(presenter.source.baseUrl + presenter.manga.url)
|
val url = Uri.parse(source.baseUrl + presenter.manga.url)
|
||||||
val intent = CustomTabsIntent.Builder()
|
val intent = CustomTabsIntent.Builder()
|
||||||
.setToolbarColor(context.theme.getResourceColor(R.attr.colorPrimary))
|
.setToolbarColor(context.theme.getResourceColor(R.attr.colorPrimary))
|
||||||
.build()
|
.build()
|
||||||
|
@ -99,7 +99,7 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
* @return manga information.
|
* @return manga information.
|
||||||
*/
|
*/
|
||||||
private fun fetchMangaObs(): Observable<Manga> {
|
private fun fetchMangaObs(): Observable<Manga> {
|
||||||
return source.pullMangaFromNetwork(manga.url)
|
return source.fetchMangaDetails(manga)
|
||||||
.flatMap { networkManga ->
|
.flatMap { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
@ -126,9 +127,16 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
observable = Observable.from(ch.pages)
|
observable = Observable.from(ch.pages)
|
||||||
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
|
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
|
||||||
} else {
|
} else {
|
||||||
observable = source.getAllImageUrlsFromPageList(ch.pages)
|
observable = source.let { source ->
|
||||||
.flatMap({ source.getCachedImage(it) }, 2)
|
if (source is OnlineSource) {
|
||||||
.doOnCompleted { source.savePageList(ch.url, ch.pages) }
|
source.fetchAllImageUrlsFromPageList(ch.pages)
|
||||||
|
.flatMap({ source.getCachedImage(it) }, 2)
|
||||||
|
.doOnCompleted { source.savePageList(ch, ch.pages) }
|
||||||
|
} else {
|
||||||
|
Observable.from(ch.pages)
|
||||||
|
.flatMap { source.fetchImage(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
observable.doOnCompleted {
|
observable.doOnCompleted {
|
||||||
if (!isSeamlessMode && chapter === ch) {
|
if (!isSeamlessMode && chapter === ch) {
|
||||||
@ -139,13 +147,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
|
|
||||||
// Listen por retry events
|
// Listen por retry events
|
||||||
add(retryPageSubject.observeOn(Schedulers.io())
|
add(retryPageSubject.observeOn(Schedulers.io())
|
||||||
.flatMap { page ->
|
.flatMap { source.fetchImage(it) }
|
||||||
if (page.imageUrl == null)
|
|
||||||
source.getImageUrlFromPage(page)
|
|
||||||
else
|
|
||||||
Observable.just<Page>(page)
|
|
||||||
}
|
|
||||||
.flatMap { source.getCachedImage(it) }
|
|
||||||
.subscribe())
|
.subscribe())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +158,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
|
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
|
||||||
else
|
else
|
||||||
// Fetch the page list from cache or fallback to network
|
// Fetch the page list from cache or fallback to network
|
||||||
source.getCachedPageListOrPullFromNetwork(chapter.url)
|
source.fetchPageList(chapter)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
@ -200,26 +202,15 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
|
|
||||||
// Preload the first pages of the next chapter. Only for non seamless mode
|
// Preload the first pages of the next chapter. Only for non seamless mode
|
||||||
private fun getPreloadNextChapterObservable(): Observable<Page> {
|
private fun getPreloadNextChapterObservable(): Observable<Page> {
|
||||||
return source.getCachedPageListOrPullFromNetwork(nextChapter!!.url)
|
val nextChapter = nextChapter ?: return Observable.error(Exception("No next chapter"))
|
||||||
|
return source.fetchPageList(nextChapter)
|
||||||
.flatMap { pages ->
|
.flatMap { pages ->
|
||||||
nextChapter!!.pages = pages
|
nextChapter.pages = pages
|
||||||
val pagesToPreload = Math.min(pages.size, 5)
|
val pagesToPreload = Math.min(pages.size, 5)
|
||||||
Observable.from(pages).take(pagesToPreload)
|
Observable.from(pages).take(pagesToPreload)
|
||||||
}
|
}
|
||||||
// Preload up to 5 images
|
// Preload up to 5 images
|
||||||
.concatMap { page ->
|
.concatMap { source.fetchImage(it) }
|
||||||
if (page.imageUrl == null)
|
|
||||||
source.getImageUrlFromPage(page)
|
|
||||||
else
|
|
||||||
Observable.just<Page>(page)
|
|
||||||
}
|
|
||||||
// Download the first image
|
|
||||||
.concatMap { page ->
|
|
||||||
if (page.pageNumber == 0)
|
|
||||||
source.getCachedImage(page)
|
|
||||||
else
|
|
||||||
Observable.just<Page>(page)
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnCompleted { stopPreloadingNextChapter() }
|
.doOnCompleted { stopPreloadingNextChapter() }
|
||||||
@ -324,7 +315,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
|
|
||||||
// Cache current page list progress for online chapters to allow a faster reopen
|
// Cache current page list progress for online chapters to allow a faster reopen
|
||||||
if (!chapter.isDownloaded) {
|
if (!chapter.isDownloaded) {
|
||||||
source.savePageList(chapter.url, pages)
|
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current progress of the chapter. Mark as read if the chapter is finished
|
// Save current progress of the chapter. Mark as read if the chapter is finished
|
||||||
@ -382,7 +373,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateMangaSyncLastChapterRead() {
|
fun updateMangaSyncLastChapterRead() {
|
||||||
for (mangaSync in mangaSyncList!!) {
|
for (mangaSync in mangaSyncList ?: emptyList()) {
|
||||||
val service = syncManager.getService(mangaSync.sync_id)
|
val service = syncManager.getService(mangaSync.sync_id)
|
||||||
if (service.isLogged && mangaSync.update) {
|
if (service.isLogged && mangaSync.update) {
|
||||||
UpdateMangaSyncService.start(context, mangaSync)
|
UpdateMangaSyncService.start(context, mangaSync)
|
||||||
@ -417,16 +408,21 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun preloadNextChapter() {
|
private fun preloadNextChapter() {
|
||||||
if (hasNextChapter() && !isChapterDownloaded(nextChapter!!)) {
|
nextChapter?.let {
|
||||||
start(PRELOAD_NEXT_CHAPTER)
|
if (!isChapterDownloaded(it)) {
|
||||||
|
start(PRELOAD_NEXT_CHAPTER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopPreloadingNextChapter() {
|
private fun stopPreloadingNextChapter() {
|
||||||
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
||||||
stop(PRELOAD_NEXT_CHAPTER)
|
stop(PRELOAD_NEXT_CHAPTER)
|
||||||
if (nextChapter!!.pages != null)
|
nextChapter?.let { chapter ->
|
||||||
source.savePageList(nextChapter!!.url, nextChapter!!.pages)
|
if (chapter.pages != null) {
|
||||||
|
source.let { if (it is OnlineSource) it.savePageList(chapter, chapter.pages) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +42,11 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
|
|||||||
.subscribe { languages ->
|
.subscribe { languages ->
|
||||||
sourcesPref.removeAll()
|
sourcesPref.removeAll()
|
||||||
|
|
||||||
val enabledSources = settingsActivity.sourceManager.getSources()
|
val enabledSources = settingsActivity.sourceManager.getOnlineSources()
|
||||||
.filter { it.lang.code in languages }
|
.filter { it.lang.code in languages }
|
||||||
|
|
||||||
for (source in enabledSources) {
|
for (source in enabledSources) {
|
||||||
if (source.isLoginRequired) {
|
if (source.isLoginRequired()) {
|
||||||
val pref = createSource(source)
|
val pref = createSource(source)
|
||||||
sourcesPref.addPreference(pref)
|
sourcesPref.addPreference(pref)
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ class SettingsSourcesFragment : SettingsNestedFragment() {
|
|||||||
fun createSource(source: Source): Preference {
|
fun createSource(source: Source): Preference {
|
||||||
return LoginPreference(preferenceManager.context).apply {
|
return LoginPreference(preferenceManager.context).apply {
|
||||||
key = preferences.keys.sourceUsername(source.id)
|
key = preferences.keys.sourceUsername(source.id)
|
||||||
title = source.visibleName
|
title = source.toString()
|
||||||
|
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
val fragment = SourceLoginDialog.newInstance(source)
|
val fragment = SourceLoginDialog.newInstance(source)
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.util
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -34,7 +35,9 @@ fun syncChaptersWithSource(db: DatabaseHelper,
|
|||||||
|
|
||||||
// Recognize number for new chapters.
|
// Recognize number for new chapters.
|
||||||
toAdd.forEach {
|
toAdd.forEach {
|
||||||
source.parseChapterNumber(it)
|
if (source is OnlineSource) {
|
||||||
|
source.parseChapterNumber(it)
|
||||||
|
}
|
||||||
ChapterRecognition.parseChapterNumber(it, manga)
|
ChapterRecognition.parseChapterNumber(it, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.util;
|
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
|
|
||||||
public final class Parser {
|
|
||||||
|
|
||||||
private Parser() throws InstantiationException {
|
|
||||||
throw new InstantiationException("This class is not for instantiation");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static Element element(Element container, String pattern) {
|
|
||||||
return container.select(pattern).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String text(Element container, String pattern) {
|
|
||||||
return text(container, pattern, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String text(Element container, String pattern, String defValue) {
|
|
||||||
Element element = container.select(pattern).first();
|
|
||||||
return element != null ? element.text() : defValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String allText(Element container, String pattern) {
|
|
||||||
Elements elements = container.select(pattern);
|
|
||||||
return !elements.isEmpty() ? elements.text() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String attr(Element container, String pattern, String attr) {
|
|
||||||
Element element = container.select(pattern).first();
|
|
||||||
return element != null ? element.attr(attr) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String href(Element container, String pattern) {
|
|
||||||
return attr(container, pattern, "href");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String src(Element container, String pattern) {
|
|
||||||
return attr(container, pattern, "src");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.widget.preference
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
@ -23,17 +24,17 @@ class SourceLoginDialog : LoginDialogPreference() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var source: Source
|
lateinit var source: OnlineSource
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val sourceId = arguments.getInt("key")
|
val sourceId = arguments.getInt("key")
|
||||||
source = (activity as SettingsActivity).sourceManager.get(sourceId)!!
|
source = (activity as SettingsActivity).sourceManager.get(sourceId) as OnlineSource
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCredentialsOnView(view: View) = with(view) {
|
override fun setCredentialsOnView(view: View) = with(view) {
|
||||||
dialog_title.text = getString(R.string.login_title, source.visibleName)
|
dialog_title.text = getString(R.string.login_title, source.toString())
|
||||||
username.setText(preferences.sourceUsername(source))
|
username.setText(preferences.sourceUsername(source))
|
||||||
password.setText(preferences.sourcePassword(source))
|
password.setText(preferences.sourcePassword(source))
|
||||||
}
|
}
|
||||||
|
26
app/src/main/res/layout/changelog_header_layout.xml
Normal file
26
app/src/main/res/layout/changelog_header_layout.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/chg_rowheader"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="@dimen/chglib_material_minHeight"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingLeft="@dimen/chglib_material_keyline1"
|
||||||
|
android:paddingRight="@dimen/chglib_material_keyline1">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ChangeLog Header [Version] You have to use the id="chg_headerVersion" -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/chg_headerVersion"
|
||||||
|
style="?android:attr/listSeparatorTextViewStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingBottom="@dimen/chglib_material_keyline1"
|
||||||
|
android:paddingTop="@dimen/chglib_material_keyline1"
|
||||||
|
android:textAppearance="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
37
app/src/main/res/layout/changelog_row_layout.xml
Normal file
37
app/src/main/res/layout/changelog_row_layout.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/chg_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="@dimen/listPreferredItemHeightSmall"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ChangeLog Row [Bullet Point] You have to use the id="chg_textbullet" -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/chg_textbullet"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
android:paddingLeft="@dimen/chglib_material_keyline1"
|
||||||
|
android:paddingRight="0dp"
|
||||||
|
android:paddingStart="@dimen/chglib_material_keyline1"
|
||||||
|
android:text="@string/changelog_row_bulletpoint"
|
||||||
|
android:textAppearance="@style/TextAppearance.Medium.Title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ChangeLog Row [Text] You have to use the id="chg_text" -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/chg_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="@dimen/listPreferredItemHeightSmall"
|
||||||
|
android:padding="@dimen/chglib_material_keyline1"
|
||||||
|
android:textAppearance="@style/TextAppearance.Regular.Body1"
|
||||||
|
tools:text="Use DashClock with Android 4.2's Daydream feature; great for use with desktop docks!"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
16
app/src/main/res/raw/changelog_debug.xml
Normal file
16
app/src/main/res/raw/changelog_debug.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<changelog bulletedList="false">
|
||||||
|
|
||||||
|
<changelogversion versionName="r736" changeDate="">
|
||||||
|
<changelogtext>[b]Important![/b] Now chapters follow the order of the sources. [b]It's required that you update your entire library
|
||||||
|
before reading in order for them to be synced.[/b] Old behavior can be restored for a manga in the overflow menu of the chapters tab.
|
||||||
|
</changelogtext>
|
||||||
|
</changelogversion>
|
||||||
|
|
||||||
|
<changelogversion versionName="r724" changeDate="">
|
||||||
|
<changelogtext>Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
|
||||||
|
from the info tab, or clearing the database (the latter won't fix covers from library manga).
|
||||||
|
</changelogtext>
|
||||||
|
</changelogversion>
|
||||||
|
|
||||||
|
</changelog>
|
4
app/src/main/res/raw/changelog_release.xml
Normal file
4
app/src/main/res/raw/changelog_release.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<changelog bulletedList="true">
|
||||||
|
|
||||||
|
</changelog>
|
@ -21,7 +21,7 @@
|
|||||||
<color name="selectorColorLight">@color/md_blue_A400_38</color>
|
<color name="selectorColorLight">@color/md_blue_A400_38</color>
|
||||||
|
|
||||||
<!-- Dark Theme -->
|
<!-- Dark Theme -->
|
||||||
<color name="colorAccentDark">@color/md_blue_A200</color>
|
<color name="colorAccentDark">#3399ff</color>
|
||||||
<color name="textColorPrimaryDark">@color/md_white_1000</color>
|
<color name="textColorPrimaryDark">@color/md_white_1000</color>
|
||||||
<color name="textColorSecondaryDark">@color/md_white_1000_70</color>
|
<color name="textColorSecondaryDark">@color/md_white_1000_70</color>
|
||||||
<color name="textColorHintDark">@color/md_white_1000_50</color>
|
<color name="textColorHintDark">@color/md_white_1000_50</color>
|
||||||
|
@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.BuildConfig;
|
|||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner;
|
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner;
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
import eu.kanade.tachiyomi.data.source.base.OnlineSource;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -32,14 +32,14 @@ public class LibraryUpdateServiceTest {
|
|||||||
ShadowApplication app;
|
ShadowApplication app;
|
||||||
Context context;
|
Context context;
|
||||||
LibraryUpdateService service;
|
LibraryUpdateService service;
|
||||||
Source source;
|
OnlineSource source;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
app = ShadowApplication.getInstance();
|
app = ShadowApplication.getInstance();
|
||||||
context = app.getApplicationContext();
|
context = app.getApplicationContext();
|
||||||
service = Robolectric.setupService(LibraryUpdateService.class);
|
service = Robolectric.setupService(LibraryUpdateService.class);
|
||||||
source = mock(Source.class);
|
source = mock(OnlineSource.class);
|
||||||
when(service.sourceManager.get(anyInt())).thenReturn(source);
|
when(service.sourceManager.get(anyInt())).thenReturn(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ public class LibraryUpdateServiceTest {
|
|||||||
|
|
||||||
List<Chapter> sourceChapters = createChapters("/chapter1", "/chapter2");
|
List<Chapter> sourceChapters = createChapters("/chapter1", "/chapter2");
|
||||||
|
|
||||||
when(source.pullChaptersFromNetwork(manga.url)).thenReturn(Observable.just(sourceChapters));
|
when(source.fetchChapterList(manga)).thenReturn(Observable.just(sourceChapters));
|
||||||
|
|
||||||
service.updateManga(manga).subscribe();
|
service.updateManga(manga).subscribe();
|
||||||
|
|
||||||
@ -79,9 +79,9 @@ public class LibraryUpdateServiceTest {
|
|||||||
List<Chapter> chapters3 = createChapters("/achapter1", "/achapter2");
|
List<Chapter> chapters3 = createChapters("/achapter1", "/achapter2");
|
||||||
|
|
||||||
// One of the updates will fail
|
// One of the updates will fail
|
||||||
when(source.pullChaptersFromNetwork("/manga1")).thenReturn(Observable.just(chapters));
|
when(source.fetchChapterList(favManga.get(0))).thenReturn(Observable.just(chapters));
|
||||||
when(source.pullChaptersFromNetwork("/manga2")).thenReturn(Observable.<List<Chapter>>error(new Exception()));
|
when(source.fetchChapterList(favManga.get(1))).thenReturn(Observable.<List<Chapter>>error(new Exception()));
|
||||||
when(source.pullChaptersFromNetwork("/manga3")).thenReturn(Observable.just(chapters3));
|
when(source.fetchChapterList(favManga.get(2))).thenReturn(Observable.just(chapters3));
|
||||||
|
|
||||||
service.updateMangaList(service.getMangaToUpdate(null)).subscribe();
|
service.updateMangaList(service.getMangaToUpdate(null)).subscribe();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user