commit 14792d2a11e5b3000072efeff85218cc9bdb1bb7 Author: len Date: Sat Jan 21 17:55:27 2017 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..59f463122 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle +/local.properties +/.idea/workspace.xml +.DS_Store +/build +/captures +.idea/ +*.iml +*/build diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 000000000..2df0c21c6 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..685bd54e2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +buildscript { + ext.kotlin_version = '1.0.6' + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.2.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + jcenter() + maven { url 'https://dl.bintray.com/inorichi/tachiyomi' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/common.gradle b/common.gradle new file mode 100644 index 000000000..28afc1b06 --- /dev/null +++ b/common.gradle @@ -0,0 +1,44 @@ +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + + buildTypes { + release { + minifyEnabled false + } + } + + sourceSets.main { + manifest.srcFile '../AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 25 + applicationIdSuffix pkgNameSuffix + versionCode extVersionCode + versionName "$libVersion.$extVersionSuffix" + manifestPlaceholders = [ + appName: appName, + extClass: extClass, + ] + } +} + +repositories { + mavenCentral() +} + +dependencies { + provided "eu.kanade.tachiyomi:extensions-library:$libVersion" + provided "com.squareup.okhttp3:okhttp:3.5.0" + provided 'io.reactivex:rxjava:1.2.4' + provided 'org.jsoup:jsoup:1.10.1' + provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} diff --git a/de-wiemanga/build.gradle b/de-wiemanga/build.gradle new file mode 100644 index 000000000..a3c4b9bca --- /dev/null +++ b/de-wiemanga/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: WieManga' + pkgNameSuffix = "de.wiemanga" + extClass = '.WieManga' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/de-wiemanga/src/eu/kanade/tachiyomi/extension/de/wiemanga/WieManga.kt b/de-wiemanga/src/eu/kanade/tachiyomi/extension/de/wiemanga/WieManga.kt new file mode 100644 index 000000000..207f21817 --- /dev/null +++ b/de-wiemanga/src/eu/kanade/tachiyomi/extension/de/wiemanga/WieManga.kt @@ -0,0 +1,122 @@ +package eu.kanade.tachiyomi.extension.de.wiemanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat + +class WieManga : ParsedHttpSource() { + + override val id: Long = 10 + + override val name = "Wie Manga!" + + override val baseUrl = "http://www.wiemanga.com" + + override val lang = "de" + + override val supportsLatest = true + + override fun popularMangaSelector() = ".booklist td > div" + + override fun latestUpdatesSelector() = ".booklist td > div" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list/Hot-Book/", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list/New-Update/", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val image = element.select("dt img") + val title = element.select("dd a:first-child") + + val manga = SManga.create() + manga.setUrlWithoutDomain(title.attr("href")) + manga.title = title.text() + manga.thumbnail_url = image.attr("src") + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = null + + override fun latestUpdatesNextPageSelector() = null + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/search/?wd=$query", headers) + } + + override fun searchMangaSelector() = ".searchresult td > div" + + override fun searchMangaFromElement(element: Element): SManga { + val image = element.select(".resultimg img") + val title = element.select(".resultbookname") + + val manga = SManga.create() + manga.setUrlWithoutDomain(title.attr("href")) + manga.title = title.text() + manga.thumbnail_url = image.attr("src") + return manga + } + + override fun searchMangaNextPageSelector() = ".pagetor a.l" + + override fun mangaDetailsParse(document: Document): SManga { + val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first() + val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first() + + val manga = SManga.create() + manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text() + manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text() + manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "") + manga.thumbnail_url = imageElement.select("img").first()?.attr("src") + + if (manga.author == "RSS") + manga.author = null + + if (manga.artist == "RSS") + manga.artist = null + return manga + } + + override fun chapterListSelector() = ".chapterlist tr:not(:first-child)" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select(".col1 a").first() + val dateElement = element.select(".col3 a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter + } + + private fun parseChapterDate(date: String): Long { + return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + document.select("select#page").first().select("option").forEach { + pages.add(Page(pages.size, it.attr("value"))) + } + return pages + } + + override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src") + +} \ No newline at end of file diff --git a/en-kissmanga/build.gradle b/en-kissmanga/build.gradle new file mode 100644 index 000000000..0b4d2f55d --- /dev/null +++ b/en-kissmanga/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Kissmanga' + pkgNameSuffix = "en.kissmanga" + extClass = '.Kissmanga' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/en-kissmanga/src/eu/kanade/tachiyomi/extension/en/kissmanga/Kissmanga.kt b/en-kissmanga/src/eu/kanade/tachiyomi/extension/en/kissmanga/Kissmanga.kt new file mode 100644 index 000000000..e9f81b919 --- /dev/null +++ b/en-kissmanga/src/eu/kanade/tachiyomi/extension/en/kissmanga/Kissmanga.kt @@ -0,0 +1,197 @@ +package eu.kanade.tachiyomi.extension.en.kissmanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.regex.Pattern + +class Kissmanga : ParsedHttpSource() { + + override val id: Long = 14 + + override val name = "Kissmanga" + + override val baseUrl = "http://kissmanga.com" + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + override fun popularMangaSelector() = "table.listing tr:gt(1)" + + override fun latestUpdatesSelector() = "table.listing tr:gt(1)" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/MangaList/MostPopular?page=$page", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("td a:eq(0)").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "li > a:contains(› Next)" + + override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val form = FormBody.Builder().apply { + add("mangaName", query) + + for (filter in if (filters.isEmpty()) getFilterList() else filters) { + when (filter) { + is Author -> add("authorArtist", filter.state) + is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state]) + is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) } + } + } + } + return POST("$baseUrl/AdvanceSearch", headers, form.build()) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun searchMangaNextPageSelector() = null + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.barContent").first() + + val manga = SManga.create() + manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text() + manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text() + manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text() + manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src") + return manga + } + + fun parseStatus(status: String) = when { + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "table.listing tr:gt(1)" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { + SimpleDateFormat("MM/dd/yyyy").parse(it).time + } ?: 0 + return chapter + } + + override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers) + + override fun pageListParse(response: Response): List { + val pages = mutableListOf() + //language=RegExp + val p = Pattern.compile("""lstImages.push\("(.+?)"""") + val m = p.matcher(response.body().string()) + + var i = 0 + while (m.find()) { + pages.add(Page(i++, "", m.group(1))) + } + return pages + } + + override fun pageListParse(document: Document): List { + throw Exception("Not used") + } + + override fun imageUrlRequest(page: Page) = GET(page.url) + + override fun imageUrlParse(document: Document) = "" + + private class Status : Filter.TriState("Completed") + private class Author : Filter.Text("Author") + private class Genre(name: String) : Filter.TriState(name) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + + override fun getFilterList() = FilterList( + Author(), + Status(), + GenreList(getGenreList()) + ) + + // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n') + // on http://kissmanga.com/AdvanceSearch + private fun getGenreList() = listOf( + Genre("4-Koma"), + Genre("Action"), + Genre("Adult"), + Genre("Adventure"), + Genre("Comedy"), + Genre("Comic"), + Genre("Cooking"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Lolicon"), + Genre("Manga"), + Genre("Manhua"), + Genre("Manhwa"), + Genre("Martial Arts"), + Genre("Mature"), + Genre("Mecha"), + Genre("Medical"), + Genre("Music"), + Genre("Mystery"), + Genre("One shot"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shotacon"), + Genre("Shoujo"), + Genre("Shoujo Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Smut"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Webtoon"), + Genre("Yaoi"), + Genre("Yuri") + ) +} \ No newline at end of file diff --git a/en-mangafox/build.gradle b/en-mangafox/build.gradle new file mode 100644 index 000000000..aa0c3c226 --- /dev/null +++ b/en-mangafox/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mangafox' + pkgNameSuffix = "en.mangafox" + extClass = '.Mangafox' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/en-mangafox/src/eu/kanade/tachiyomi/extension/en/mangafox/Mangafox.kt b/en-mangafox/src/eu/kanade/tachiyomi/extension/en/mangafox/Mangafox.kt new file mode 100644 index 000000000..b93a81280 --- /dev/null +++ b/en-mangafox/src/eu/kanade/tachiyomi/extension/en/mangafox/Mangafox.kt @@ -0,0 +1,223 @@ +package eu.kanade.tachiyomi.extension.en.mangafox + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class Mangafox : ParsedHttpSource() { + + override val id: Long = 3 + + override val name = "Mangafox" + + override val baseUrl = "http://mangafox.me" + + override val lang = "en" + + override val supportsLatest = true + + override fun popularMangaSelector() = "div#mangalist > ul.list > li" + + override fun popularMangaRequest(page: Int): Request { + val pageStr = if (page != 1) "$page.htm" else "" + return GET("$baseUrl/directory/$pageStr", headers) + } + + override fun latestUpdatesSelector() = "div#mangalist > ul.list > li" + + override fun latestUpdatesRequest(page: Int): Request { + val pageStr = if (page != 1) "$page.htm" else "" + return GET("$baseUrl/directory/$pageStr?latest") + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a.title").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "a:has(span.next)" + + override fun latestUpdatesNextPageSelector() = "a:has(span.next)" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is Status -> url.addQueryParameter(filter.id, filter.state.toString()) + is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } + is TextField -> url.addQueryParameter(filter.key, filter.state) + is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString()) + is OrderBy -> { + url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) + url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") + } + } + } + url.addQueryParameter("page", page.toString()) + return GET(url.toString(), headers) + } + + override fun searchMangaSelector() = "div#mangalist > ul.list > li" + + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a.title").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun searchMangaNextPageSelector() = "a:has(span.next)" + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div#title").first() + val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() + val sideInfoElement = document.select("#series_info").first() + + val manga = SManga.create() + manga.author = rowElement.select("td:eq(1)").first()?.text() + manga.artist = rowElement.select("td:eq(2)").first()?.text() + manga.genre = rowElement.select("td:eq(3)").first()?.text() + manga.description = infoElement.select("p.summary").first()?.text() + manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") + return manga + } + + private fun parseStatus(status: String) = when { + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "div#chapters li div" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a.tips").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter + } + + private fun parseChapterDate(date: String): Long { + return if ("Today" in date || " ago" in date) { + Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + } else if ("Yesterday" in date) { + Calendar.getInstance().apply { + add(Calendar.DATE, -1) + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + } else { + try { + SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + 0L + } + } + } + + override fun pageListParse(document: Document): List { + val url = document.baseUri().substringBeforeLast('/') + + val pages = mutableListOf() + document.select("select.m").first()?.select("option:not([value=0])")?.forEach { + pages.add(Page(pages.size, "$url/${it.attr("value")}.html")) + } + return pages + } + + override fun imageUrlParse(document: Document): String { + val url = document.getElementById("image").attr("src") + return if ("compressed?token=" !in url) { + url + } else { + "http://mangafox.me/media/logo.png" + } + } + + private class Status(val id: String = "is_completed") : Filter.TriState("Completed") + private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name) + private class TextField(name: String, val key: String) : Filter.Text(name) + private class Type : Filter.Select("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua")) + private class OrderBy : Filter.Sort("Order by", + arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), + Selection(2, false)) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + + override fun getFilterList() = FilterList( + TextField("Author", "author"), + TextField("Artist", "artist"), + Type(), + Status(), + OrderBy(), + GenreList(getGenreList()) + ) + + // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n') + // on http://mangafox.me/search.php + private fun getGenreList() = listOf( + Genre("Action"), + Genre("Adult"), + Genre("Adventure"), + Genre("Comedy"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Martial Arts"), + Genre("Mature"), + Genre("Mecha"), + Genre("Mystery"), + Genre("One Shot"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shoujo"), + Genre("Shoujo Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Smut"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Webtoons"), + Genre("Yaoi"), + Genre("Yuri") + ) + +} \ No newline at end of file diff --git a/en-mangahere/build.gradle b/en-mangahere/build.gradle new file mode 100644 index 000000000..fcd347a8a --- /dev/null +++ b/en-mangahere/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mangahere' + pkgNameSuffix = "en.mangahere" + extClass = '.Mangahere' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/en-mangahere/src/eu/kanade/tachiyomi/extension/en/mangahere/Mangahere.kt b/en-mangahere/src/eu/kanade/tachiyomi/extension/en/mangahere/Mangahere.kt new file mode 100644 index 000000000..2fc871a14 --- /dev/null +++ b/en-mangahere/src/eu/kanade/tachiyomi/extension/en/mangahere/Mangahere.kt @@ -0,0 +1,220 @@ +package eu.kanade.tachiyomi.extension.en.mangahere + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class Mangahere : ParsedHttpSource() { + + override val id: Long = 2 + + override val name = "Mangahere" + + override val baseUrl = "http://www.mangahere.co" + + override val lang = "en" + + override val supportsLatest = true + + override fun popularMangaSelector() = "div.directory_list > ul > li" + + override fun latestUpdatesSelector() = "div.directory_list > ul > li" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/directory/$page.htm?views.za", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers) + } + + private fun mangaFromElement(query: String, element: Element): SManga { + val manga = SManga.create() + element.select(query).first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text() + } + return manga + } + + override fun popularMangaFromElement(element: Element): SManga { + return mangaFromElement("div.title > a", element) + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "div.next-page > a.next" + + override fun latestUpdatesNextPageSelector() = "div.next-page > a.next" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query) + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state]) + is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } + is TextField -> url.addQueryParameter(filter.key, filter.state) + is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state]) + is OrderBy -> { + url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) + url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") + } + } + } + url.addQueryParameter("page", page.toString()) + return GET(url.toString(), headers) + } + + override fun searchMangaSelector() = "div.result_search > dl:has(dt)" + + override fun searchMangaFromElement(element: Element): SManga { + return mangaFromElement("a.manga_info", element) + } + + override fun searchMangaNextPageSelector() = "div.next-page > a.next" + + override fun mangaDetailsParse(document: Document): SManga { + val detailElement = document.select(".manga_detail_top").first() + val infoElement = detailElement.select(".detail_topText").first() + + val manga = SManga.create() + manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() + manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() + manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") + manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") + manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src") + return manga + } + + private fun parseStatus(status: String) = when { + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = ".detail_list > ul:not([class]) > li" + + override fun chapterFromElement(element: Element): SChapter { + val parentEl = element.select("span.left").first() + + val urlElement = parentEl.select("a").first() + + var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: "" + if (volume.length > 0) { + volume = " - " + volume + } + + var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: "" + if (title.length > 0) { + title = " - " + title + } + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + volume + title + chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter + } + + private fun parseChapterDate(date: String): Long { + return if ("Today" in date) { + Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + } else if ("Yesterday" in date) { + Calendar.getInstance().apply { + add(Calendar.DATE, -1) + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + } else { + try { + SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + 0L + } + } + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { + pages.add(Page(pages.size, it.attr("value"))) + } + pages.getOrNull(0)?.imageUrl = imageUrlParse(document) + return pages + } + + override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src") + + private class Status : Filter.TriState("Completed") + private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name) + private class TextField(name: String, val key: String) : Filter.Text(name) + private class Type : Filter.Select("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)")) + private class OrderBy : Filter.Sort("Order by", + arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), + Selection(2, false)) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + + override fun getFilterList() = FilterList( + TextField("Author", "author"), + TextField("Artist", "artist"), + Type(), + Status(), + OrderBy(), + GenreList(getGenreList()) + ) + + // [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n') + // http://www.mangahere.co/advsearch.htm + private fun getGenreList() = listOf( + Genre("Action"), + Genre("Adventure"), + Genre("Comedy"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Martial Arts"), + Genre("Mature"), + Genre("Mecha"), + Genre("Mystery"), + Genre("One Shot"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shoujo"), + Genre("Shoujo Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Yaoi"), + Genre("Yuri") + ) + +} \ No newline at end of file diff --git a/en-mangasee/build.gradle b/en-mangasee/build.gradle new file mode 100644 index 000000000..b525f9d37 --- /dev/null +++ b/en-mangasee/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mangasee' + pkgNameSuffix = "en.mangasee" + extClass = '.Mangasee' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/en-mangasee/src/eu/kanade/tachiyomi/extension/en/mangasee/Mangasee.kt b/en-mangasee/src/eu/kanade/tachiyomi/extension/en/mangasee/Mangasee.kt new file mode 100644 index 000000000..5a7d9e229 --- /dev/null +++ b/en-mangasee/src/eu/kanade/tachiyomi/extension/en/mangasee/Mangasee.kt @@ -0,0 +1,243 @@ +package eu.kanade.tachiyomi.extension.en.mangasee + +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.FormBody +import okhttp3.HttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.regex.Pattern + +class Mangasee : ParsedHttpSource() { + + override val id: Long = 9 + + override val name = "Mangasee" + + override val baseUrl = "http://mangaseeonline.net" + + override val lang = "en" + + override val supportsLatest = true + + private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?") + + private val indexPattern = Pattern.compile("-index-(.*?)-") + + override fun popularMangaSelector() = "div.requested > div.row" + + override fun popularMangaRequest(page: Int): Request { + val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending") + return POST(requestUrl, headers, body.build()) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a.resultLink").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun popularMangaNextPageSelector() = "button.requestMore" + + override fun searchMangaSelector() = "div.requested > div.row" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder() + if (!query.isEmpty()) url.addQueryParameter("keyword", query) + val genres = mutableListOf() + val genresNo = mutableListOf() + for (filter in if (filters.isEmpty()) getFilterList() else filters) { + when (filter) { + is Sort -> { + if (filter.state?.index != 0) + url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity") + if (filter.state?.ascending != true) + url.addQueryParameter("sortOrder", "descending") + } + is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state]) + is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state) + is GenreList -> filter.state.forEach { genre -> + when (genre.state) { + Filter.TriState.STATE_INCLUDE -> genres.add(genre.name) + Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name) + } + } + } + } + if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(",")) + if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(",")) + + val (body, requestUrl) = convertQueryToPost(page, url.toString()) + return POST(requestUrl, headers, body.build()) + } + + private fun convertQueryToPost(page: Int, url: String): Pair { + val url = HttpUrl.parse(url) + val body = FormBody.Builder().add("page", page.toString()) + for (i in 0..url.querySize() - 1) { + body.add(url.queryParameterName(i), url.queryParameterValue(i)) + } + val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath() + return Pair(body, requestUrl) + } + + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a.resultLink").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun searchMangaNextPageSelector() = "button.requestMore" + + override fun mangaDetailsParse(document: Document): SManga { + val detailElement = document.select("div.well > div.row").first() + + val manga = SManga.create() + manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text() + manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString() + manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text() + manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src") + return manga + } + + private fun parseStatus(status: String) = when { + status.contains("Ongoing (Scan)") -> SManga.ONGOING + status.contains("Complete (Scan)") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "div.chapter-list > a" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: "" + chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0 + return chapter + } + + private fun parseChapterDate(dateAsString: String): Long { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time + } + + override fun pageListParse(document: Document): List { + val fullUrl = document.baseUri() + val url = fullUrl.substringBeforeLast('/') + + val pages = mutableListOf() + + val series = document.select("input.IndexName").first().attr("value") + val chapter = document.select("span.CurChapter").first().text() + var index = "" + + val m = indexPattern.matcher(fullUrl) + if (m.find()) { + val indexNumber = m.group(1) + index = "-index-$indexNumber" + } + + document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach { + pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html")) + } + pages.getOrNull(0)?.imageUrl = imageUrlParse(document) + return pages + } + + override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src") + + override fun latestUpdatesNextPageSelector() = "button.requestMore" + + override fun latestUpdatesSelector(): String = "a.latestSeries" + + override fun latestUpdatesRequest(page: Int): Request { + val url = "http://mangaseeonline.net/home/latest.request.php" + val (body, requestUrl) = convertQueryToPost(page, url) + return POST(requestUrl, headers, body.build()) + } + + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a.latestSeries").first().let { + val chapterUrl = it.attr("href") + val indexOfMangaUrl = chapterUrl.indexOf("-chapter-") + val indexOfLastPath = chapterUrl.lastIndexOf("/") + val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl) + val defaultText = it.select("p.clamp2").text() + val m = recentUpdatesPattern.matcher(defaultText) + val title = if (m.matches()) m.group(1) else defaultText + manga.setUrlWithoutDomain("/manga" + mangaUrl) + manga.title = title + } + return manga + } + + private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false)) + private class Genre(name: String) : Filter.TriState(name) + private class TextField(name: String, val key: String) : Filter.Text(name) + private class SelectField(name: String, val key: String, values: Array, state: Int = 0) : Filter.Select(name, values, state) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + + override fun getFilterList() = FilterList( + TextField("Years", "year"), + TextField("Author", "author"), + SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")), + SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")), + SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")), + Sort(), + GenreList(getGenreList()) + ) + + // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n') + // http://mangasee.co/advanced-search/ + private fun getGenreList() = listOf( + Genre("Action"), + Genre("Adult"), + Genre("Adventure"), + Genre("Comedy"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Hentai"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Lolicon"), + Genre("Martial Arts"), + Genre("Mature"), + Genre("Mecha"), + Genre("Mystery"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shotacon"), + Genre("Shoujo"), + Genre("Shoujo Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Smut"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Yaoi"), + Genre("Yuri") + ) + +} \ No newline at end of file diff --git a/en-readmangatoday/build.gradle b/en-readmangatoday/build.gradle new file mode 100644 index 000000000..09f7fcc8f --- /dev/null +++ b/en-readmangatoday/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: ReadMangaToday' + pkgNameSuffix = "en.readmangatoday" + extClass = '.Readmangatoday' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/en-readmangatoday/src/eu/kanade/tachiyomi/extension/en/readmangatoday/Readmangatoday.kt b/en-readmangatoday/src/eu/kanade/tachiyomi/extension/en/readmangatoday/Readmangatoday.kt new file mode 100644 index 000000000..f30e9b3dd --- /dev/null +++ b/en-readmangatoday/src/eu/kanade/tachiyomi/extension/en/readmangatoday/Readmangatoday.kt @@ -0,0 +1,219 @@ +package eu.kanade.tachiyomi.extension.en.readmangatoday + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.util.* + +class Readmangatoday : ParsedHttpSource() { + + override val id: Long = 8 + + override val name = "ReadMangaToday" + + override val baseUrl = "http://www.readmanga.today" + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient get() = network.cloudflareClient + + /** + * Search only returns data with this set + */ + override fun headersBuilder() = Headers.Builder().apply { + add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") + add("X-Requested-With", "XMLHttpRequest") + } + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/hot-manga/$page", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/latest-releases/$page", headers) + } + + override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box" + + override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("div.title > h2 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" + + override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val builder = okhttp3.FormBody.Builder() + builder.add("manga-name", query) + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is TextField -> builder.add(filter.key, filter.state) + is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state]) + is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state]) + is GenreList -> filter.state.forEach { genre -> + when (genre.state) { + Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString()) + Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString()) + } + } + } + } + return POST("$baseUrl/service/advanced_search", headers, builder.build()) + } + + override fun searchMangaSelector() = "div.style-list > div.box" + + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("div.title > h2 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun searchMangaNextPageSelector() = "div.next-page > a.next" + + override fun mangaDetailsParse(document: Document): SManga { + val detailElement = document.select("div.movie-meta").first() + + val manga = SManga.create() + manga.author = document.select("ul.cast-list li.director > ul a").first()?.text() + manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text() + manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text() + manga.description = detailElement.select("li.movie-detail").first()?.text() + manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src") + return manga + } + + private fun parseStatus(status: String) = when { + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "ul.chp_lst > li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.select("span.val").text() + chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0 + return chapter + } + + private fun parseChapterDate(date: String): Long { + val dateWords: List = date.split(" ") + + if (dateWords.size == 3) { + val timeAgo = Integer.parseInt(dateWords[0]) + val date: Calendar = Calendar.getInstance() + + if (dateWords[1].contains("Minute")) { + date.add(Calendar.MINUTE, -timeAgo) + } else if (dateWords[1].contains("Hour")) { + date.add(Calendar.HOUR_OF_DAY, -timeAgo) + } else if (dateWords[1].contains("Day")) { + date.add(Calendar.DAY_OF_YEAR, -timeAgo) + } else if (dateWords[1].contains("Week")) { + date.add(Calendar.WEEK_OF_YEAR, -timeAgo) + } else if (dateWords[1].contains("Month")) { + date.add(Calendar.MONTH, -timeAgo) + } else if (dateWords[1].contains("Year")) { + date.add(Calendar.YEAR, -timeAgo) + } + + return date.timeInMillis + } + + return 0L + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach { + pages.add(Page(pages.size, it.attr("value"))) + } + pages.getOrNull(0)?.imageUrl = imageUrlParse(document) + return pages + } + + override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") + + private class Status : Filter.TriState("Completed") + private class Genre(name: String, val id: Int) : Filter.TriState(name) + private class TextField(name: String, val key: String) : Filter.Text(name) + private class Type : Filter.Select("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua")) + private class GenreList(genres: List) : Filter.Group("Genres", genres) + + override fun getFilterList() = FilterList( + TextField("Author", "author-name"), + TextField("Artist", "artist-name"), + Type(), + Status(), + GenreList(getGenreList()) + ) + + // [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n') + // http://www.readmanga.today/advanced-search + private fun getGenreList() = listOf( + Genre("Action", 2), + Genre("Adventure", 4), + Genre("Comedy", 5), + Genre("Doujinshi", 6), + Genre("Drama", 7), + Genre("Ecchi", 8), + Genre("Fantasy", 9), + Genre("Gender Bender", 10), + Genre("Harem", 11), + Genre("Historical", 12), + Genre("Horror", 13), + Genre("Josei", 14), + Genre("Lolicon", 15), + Genre("Martial Arts", 16), + Genre("Mature", 17), + Genre("Mecha", 18), + Genre("Mystery", 19), + Genre("One shot", 20), + Genre("Psychological", 21), + Genre("Romance", 22), + Genre("School Life", 23), + Genre("Sci-fi", 24), + Genre("Seinen", 25), + Genre("Shotacon", 26), + Genre("Shoujo", 27), + Genre("Shoujo Ai", 28), + Genre("Shounen", 29), + Genre("Shounen Ai", 30), + Genre("Slice of Life", 31), + Genre("Smut", 32), + Genre("Sports", 33), + Genre("Supernatural", 34), + Genre("Tragedy", 35), + Genre("Yaoi", 36), + Genre("Yuri", 37) + ) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..aac7c9b46 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..13372aef5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..04e285f34 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..9d82f7891 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ru-mangachan/build.gradle b/ru-mangachan/build.gradle new file mode 100644 index 000000000..a65789a62 --- /dev/null +++ b/ru-mangachan/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mangachan' + pkgNameSuffix = "ru.mangachan" + extClass = '.Mangachan' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/ru-mangachan/src/eu/kanade/tachiyomi/extension/ru/mangachan/Mangachan.kt b/ru-mangachan/src/eu/kanade/tachiyomi/extension/ru/mangachan/Mangachan.kt new file mode 100644 index 000000000..37195f185 --- /dev/null +++ b/ru-mangachan/src/eu/kanade/tachiyomi/extension/ru/mangachan/Mangachan.kt @@ -0,0 +1,230 @@ +package eu.kanade.tachiyomi.extension.ru.mangachan + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.* + +class Mangachan : ParsedHttpSource() { + + override val id: Long = 7 + + override val name = "Mangachan" + + override val baseUrl = "http://mangachan.me" + + override val lang = "ru" + + override val supportsLatest = true + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if (query.isNotEmpty()) { + "$baseUrl/?do=search&subaction=search&story=$query" + } else { + val filt = filters.filterIsInstance().filter { !it.isIgnored() } + if (filt.isNotEmpty()) { + var genres = "" + filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' } + "$baseUrl/tags/${genres.dropLast(1)}" + } else { + "$baseUrl/?do=search&subaction=search&story=$query" + } + } + return GET(url, headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/newestch?page=$page") + } + + override fun popularMangaSelector() = "div.content_row" + + override fun latestUpdatesSelector() = "ul.area_rightNews li" + + override fun searchMangaSelector() = popularMangaSelector() + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("h2 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a:nth-child(1)").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "a:contains(Вперед)" + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + override fun searchMangaNextPageSelector() = "a:contains(Далее)" + + private fun searchGenresNextPageSelector() = popularMangaNextPageSelector() + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + val mangas = document.select(searchMangaSelector()).map { element -> + searchMangaFromElement(element) + } + + // FIXME +// val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE } +// searchMangaNextPageSelector().let { selector -> +// if (page.nextPageUrl.isNullOrEmpty() && allIgnore) { +// val onClick = document.select(selector).first()?.attr("onclick") +// val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)")) +// page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum +// } +// } +// +// searchGenresNextPageSelector().let { selector -> +// if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) { +// val url = document.select(selector).first()?.attr("href") +// page.nextPageUrl = searchMangaInitialUrl(query, filters) + url +// } +// } + + return MangasPage(mangas, false) + } + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("table.mangatitle").first() + val descElement = document.select("div#description").first() + val imgElement = document.select("img#cover").first() + + val manga = SManga.create() + manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text() + manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() + manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) + manga.description = descElement.textNodes().first().text() + manga.thumbnail_url = baseUrl + imgElement.attr("src") + return manga + } + + private fun parseStatus(element: String): Int { + when { + element.contains("перевод завершен") -> return SManga.COMPLETED + element.contains("перевод продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN + } + } + + override fun chapterListSelector() = "table.table_cha tr:gt(1)" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = element.select("div.date").first()?.text()?.let { + SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time + } ?: 0 + return chapter + } + + override fun pageListParse(response: Response): List { + val html = response.body().string() + val beginIndex = html.indexOf("fullimg\":[") + 10 + val endIndex = html.indexOf(",]", beginIndex) + val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "") + val pageUrls = trimmedHtml.split(',') + + return pageUrls.mapIndexed { i, url -> Page(i, "", url) } + } + + override fun pageListParse(document: Document): List { + throw Exception("Not used") + } + + override fun imageUrlParse(document: Document) = "" + + private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name) + + /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) => + * { const link=el.getAttribute('href');const id=link.substr(6,link.length); + * return `Genre("${id.replace("_", " ")}")` }).join(',\n') + * on http://mangachan.me/ + */ + override fun getFilterList() = FilterList( + Genre("18 плюс"), + Genre("bdsm"), + Genre("арт"), + Genre("биография"), + Genre("боевик"), + Genre("боевые искусства"), + Genre("вампиры"), + Genre("веб"), + Genre("гарем"), + Genre("гендерная интрига"), + Genre("героическое фэнтези"), + Genre("детектив"), + Genre("дзёсэй"), + Genre("додзинси"), + Genre("драма"), + Genre("игра"), + Genre("инцест"), + Genre("искусство"), + Genre("история"), + Genre("киберпанк"), + Genre("кодомо"), + Genre("комедия"), + Genre("литРПГ"), + Genre("магия"), + Genre("махо-сёдзё"), + Genre("меха"), + Genre("мистика"), + Genre("музыка"), + Genre("научная фантастика"), + Genre("повседневность"), + Genre("постапокалиптика"), + Genre("приключения"), + Genre("психология"), + Genre("романтика"), + Genre("самурайский боевик"), + Genre("сборник"), + Genre("сверхъестественное"), + Genre("сказка"), + Genre("спорт"), + Genre("супергерои"), + Genre("сэйнэн"), + Genre("сёдзё"), + Genre("сёдзё-ай"), + Genre("сёнэн"), + Genre("сёнэн-ай"), + Genre("тентакли"), + Genre("трагедия"), + Genre("триллер"), + Genre("ужасы"), + Genre("фантастика"), + Genre("фурри"), + Genre("фэнтези"), + Genre("школа"), + Genre("эротика"), + Genre("юри"), + Genre("яой"), + Genre("ёнкома") + ) +} \ No newline at end of file diff --git a/ru-mintmanga/build.gradle b/ru-mintmanga/build.gradle new file mode 100644 index 000000000..752a6aa85 --- /dev/null +++ b/ru-mintmanga/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mintmanga' + pkgNameSuffix = "ru.mintmanga" + extClass = '.Mintmanga' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/ru-mintmanga/src/eu/kanade/tachiyomi/extension/ru/mintmanga/Mintmanga.kt b/ru-mintmanga/src/eu/kanade/tachiyomi/extension/ru/mintmanga/Mintmanga.kt new file mode 100644 index 000000000..39c1fc5bf --- /dev/null +++ b/ru-mintmanga/src/eu/kanade/tachiyomi/extension/ru/mintmanga/Mintmanga.kt @@ -0,0 +1,184 @@ +package eu.kanade.tachiyomi.extension.ru.mintmanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern + +class Mintmanga : ParsedHttpSource() { + + override val id: Long = 6 + + override val name = "Mintmanga" + + override val baseUrl = "http://mintmanga.com" + + override val lang = "ru" + + override val supportsLatest = true + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun popularMangaSelector() = "div.desc" + + override fun latestUpdatesSelector() = "div.desc" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("h3 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "a.nextLink" + + override fun latestUpdatesNextPageSelector() = "a.nextLink" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") + return GET("$baseUrl/search?q=$query&$genres", headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + // max 200 results + override fun searchMangaNextPageSelector() = null + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.leftContent").first() + + val manga = SManga.create() + manga.author = infoElement.select("span.elem_author").first()?.text() + manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") + manga.description = infoElement.select("div.manga-description").text() + manga.status = parseStatus(infoElement.html()) + manga.thumbnail_url = infoElement.select("img").attr("data-full") + return manga + } + + private fun parseStatus(element: String): Int { + when { + element.contains("

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

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

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED + element.contains("Перевод: продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN + } + } + + override fun chapterListSelector() = "div.chapters-link tbody tr" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") + chapter.name = urlElement.text().replace(" новое", "") + chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { + SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time + } ?: 0 + return chapter + } + + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + chapter.chapter_number = -2f + } + + override fun pageListParse(response: Response): List { + val html = response.body().string() + val beginIndex = html.indexOf("rm_h.init( [") + val endIndex = html.indexOf("], 0, false);", beginIndex) + val trimmedHtml = html.substring(beginIndex, endIndex) + + val p = Pattern.compile("'.+?','.+?',\".+?\"") + val m = p.matcher(trimmedHtml) + + val pages = mutableListOf() + + var i = 0 + while (m.find()) { + val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') + pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) + } + return pages + } + + override fun pageListParse(document: Document): List { + throw Exception("Not used") + } + + override fun imageUrlParse(document: Document) = "" + + private class Genre(name: String, val id: String) : Filter.TriState(name) + + /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { + * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); + * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') + * on http://mintmanga.com/search + */ + override fun getFilterList() = FilterList( + Genre("арт", "el_2220"), + Genre("бара", "el_1353"), + Genre("боевик", "el_1346"), + Genre("боевые искусства", "el_1334"), + Genre("вампиры", "el_1339"), + Genre("гарем", "el_1333"), + Genre("гендерная интрига", "el_1347"), + Genre("героическое фэнтези", "el_1337"), + Genre("детектив", "el_1343"), + Genre("дзёсэй", "el_1349"), + Genre("додзинси", "el_1332"), + Genre("драма", "el_1310"), + Genre("игра", "el_5229"), + Genre("история", "el_1311"), + Genre("киберпанк", "el_1351"), + Genre("комедия", "el_1328"), + Genre("меха", "el_1318"), + Genre("мистика", "el_1324"), + Genre("научная фантастика", "el_1325"), + Genre("повседневность", "el_1327"), + Genre("постапокалиптика", "el_1342"), + Genre("приключения", "el_1322"), + Genre("психология", "el_1335"), + Genre("романтика", "el_1313"), + Genre("самурайский боевик", "el_1316"), + Genre("сверхъестественное", "el_1350"), + Genre("сёдзё", "el_1314"), + Genre("сёдзё-ай", "el_1320"), + Genre("сёнэн", "el_1326"), + Genre("сёнэн-ай", "el_1330"), + Genre("спорт", "el_1321"), + Genre("сэйнэн", "el_1329"), + Genre("трагедия", "el_1344"), + Genre("триллер", "el_1341"), + Genre("ужасы", "el_1317"), + Genre("фантастика", "el_1331"), + Genre("фэнтези", "el_1323"), + Genre("школа", "el_1319"), + Genre("эротика", "el_1340"), + Genre("этти", "el_1354"), + Genre("юри", "el_1315"), + Genre("яой", "el_1336") + ) +} \ No newline at end of file diff --git a/ru-readmanga/build.gradle b/ru-readmanga/build.gradle new file mode 100644 index 000000000..bf88ad5e4 --- /dev/null +++ b/ru-readmanga/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Readmanga' + pkgNameSuffix = "ru.readmanga" + extClass = '.Readmanga' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: '../common.gradle' diff --git a/ru-readmanga/src/eu/kanade/tachiyomi/extension/ru/readmanga/Readmanga.kt b/ru-readmanga/src/eu/kanade/tachiyomi/extension/ru/readmanga/Readmanga.kt new file mode 100644 index 000000000..89b4f9cd9 --- /dev/null +++ b/ru-readmanga/src/eu/kanade/tachiyomi/extension/ru/readmanga/Readmanga.kt @@ -0,0 +1,183 @@ +package eu.kanade.tachiyomi.extension.ru.readmanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern + +class Readmanga : ParsedHttpSource() { + + override val id: Long = 5 + + override val name = "Readmanga" + + override val baseUrl = "http://readmanga.me" + + override val lang = "ru" + + override val supportsLatest = true + + override fun popularMangaSelector() = "div.desc" + + override fun latestUpdatesSelector() = "div.desc" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("h3 > a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.attr("title") + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "a.nextLink" + + override fun latestUpdatesNextPageSelector() = "a.nextLink" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val genres = filters.filterIsInstance().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") + return GET("$baseUrl/search?q=$query&$genres", headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + // max 200 results + override fun searchMangaNextPageSelector() = null + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.leftContent").first() + + val manga = SManga.create() + manga.author = infoElement.select("span.elem_author").first()?.text() + manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") + manga.description = infoElement.select("div.manga-description").text() + manga.status = parseStatus(infoElement.html()) + manga.thumbnail_url = infoElement.select("img").attr("data-full") + return manga + } + + private fun parseStatus(element: String): Int { + when { + element.contains("

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

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

Сингл") || element.contains("Перевод: завершен") -> return SManga.COMPLETED + element.contains("Перевод: продолжается") -> return SManga.ONGOING + else -> return SManga.UNKNOWN + } + } + + override fun chapterListSelector() = "div.chapters-link tbody tr" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") + chapter.name = urlElement.text().replace(" новое", "") + chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { + SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time + } ?: 0 + return chapter + } + + override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + chapter.chapter_number = -2f + } + + override fun pageListParse(response: Response): List { + val html = response.body().string() + val beginIndex = html.indexOf("rm_h.init( [") + val endIndex = html.indexOf("], 0, false);", beginIndex) + val trimmedHtml = html.substring(beginIndex, endIndex) + + val p = Pattern.compile("'.+?','.+?',\".+?\"") + val m = p.matcher(trimmedHtml) + + val pages = mutableListOf() + + var i = 0 + while (m.find()) { + val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') + pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) + } + return pages + } + + override fun pageListParse(document: Document): List { + throw Exception("Not used") + } + + override fun imageUrlParse(document: Document) = "" + + private class Genre(name: String, val id: String) : Filter.TriState(name) + + /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { + * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); + * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') + * on http://readmanga.me/search + */ + override fun getFilterList() = FilterList( + Genre("арт", "el_5685"), + Genre("боевик", "el_2155"), + Genre("боевые искусства", "el_2143"), + Genre("вампиры", "el_2148"), + Genre("гарем", "el_2142"), + Genre("гендерная интрига", "el_2156"), + Genre("героическое фэнтези", "el_2146"), + Genre("детектив", "el_2152"), + Genre("дзёсэй", "el_2158"), + Genre("додзинси", "el_2141"), + Genre("драма", "el_2118"), + Genre("игра", "el_2154"), + Genre("история", "el_2119"), + Genre("киберпанк", "el_8032"), + Genre("кодомо", "el_2137"), + Genre("комедия", "el_2136"), + Genre("махо-сёдзё", "el_2147"), + Genre("меха", "el_2126"), + Genre("мистика", "el_2132"), + Genre("научная фантастика", "el_2133"), + Genre("повседневность", "el_2135"), + Genre("постапокалиптика", "el_2151"), + Genre("приключения", "el_2130"), + Genre("психология", "el_2144"), + Genre("романтика", "el_2121"), + Genre("самурайский боевик", "el_2124"), + Genre("сверхъестественное", "el_2159"), + Genre("сёдзё", "el_2122"), + Genre("сёдзё-ай", "el_2128"), + Genre("сёнэн", "el_2134"), + Genre("сёнэн-ай", "el_2139"), + Genre("спорт", "el_2129"), + Genre("сэйнэн", "el_2138"), + Genre("трагедия", "el_2153"), + Genre("триллер", "el_2150"), + Genre("ужасы", "el_2125"), + Genre("фантастика", "el_2140"), + Genre("фэнтези", "el_2131"), + Genre("школа", "el_2127"), + Genre("этти", "el_2149"), + Genre("юри", "el_2123") + ) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..f2efd37fc --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +include 'en-mangafox', + 'en-mangahere', + 'en-kissmanga', + 'en-mangasee', + 'en-readmangatoday', + 'de-wiemanga', + 'ru-mangachan', + 'ru-mintmanga', + 'ru-readmanga'