diff --git a/src/all/thelibraryofohara/build.gradle b/src/all/thelibraryofohara/build.gradle new file mode 100644 index 000000000..7a68b4416 --- /dev/null +++ b/src/all/thelibraryofohara/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'The Library of Ohara' + pkgNameSuffix = 'all.thelibraryofohara' + extClass = '.TheLibraryOfOharaFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/thelibraryofohara/res/mipmap-hdpi/ic_launcher.png b/src/all/thelibraryofohara/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..cb0ef243e Binary files /dev/null and b/src/all/thelibraryofohara/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/thelibraryofohara/res/mipmap-mdpi/ic_launcher.png b/src/all/thelibraryofohara/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..de139bb84 Binary files /dev/null and b/src/all/thelibraryofohara/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/thelibraryofohara/res/mipmap-xhdpi/ic_launcher.png b/src/all/thelibraryofohara/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2599895fd Binary files /dev/null and b/src/all/thelibraryofohara/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/thelibraryofohara/res/mipmap-xxhdpi/ic_launcher.png b/src/all/thelibraryofohara/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6d7b4fc33 Binary files /dev/null and b/src/all/thelibraryofohara/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/thelibraryofohara/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/thelibraryofohara/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..7c06f583e Binary files /dev/null and b/src/all/thelibraryofohara/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/thelibraryofohara/res/web_hi_res_512.png b/src/all/thelibraryofohara/res/web_hi_res_512.png new file mode 100644 index 000000000..d6b8aba25 Binary files /dev/null and b/src/all/thelibraryofohara/res/web_hi_res_512.png differ diff --git a/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOhara.kt b/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOhara.kt new file mode 100644 index 000000000..168867f82 --- /dev/null +++ b/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOhara.kt @@ -0,0 +1,210 @@ +package eu.kanade.tachiyomi.extension.all.thelibraryofohara + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import java.text.SimpleDateFormat +import java.util.Locale +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class TheLibraryOfOhara(override val lang: String, private val siteLang: String) : ParsedHttpSource() { + + override val name = "The Library of Ohara" + + override val baseUrl = "https://thelibraryofohara.com" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET(baseUrl, headers) + } + + // only show entries which contain pictures only. + override fun popularMangaSelector() = when (lang) { + "en" -> "#categories-7 ul li.cat-item-589813936," + // Chapter Secrets + "#categories-7 ul li.cat-item-607613583, " + // Chapter Secrets Specials + "#categories-7 ul li.cat-item-43972770, " + // Charlotte Family + "#categories-7 ul li.cat-item-9363667, " + // Complete Guides + "#categories-7 ul li.cat-item-634609261, " + // Parody Chapter + "#categories-7 ul li.cat-item-699200615, " + // Return to the Reverie + "#categories-7 ul li.cat-item-139757, " + // SBS + "#categories-7 ul li.cat-item-22695, " + // Timeline + "#categories-7 ul li.cat-item-648324575" // Vivre Card Databook + "id" -> "#categories-7 ul li.cat-item-702404482, #categories-7 ul li.cat-item-699200615" // Chapter Secrets Bahasa Indonesia, Return to the Reverie + "fr" -> "#categories-7 ul li.cat-item-699200615" // Return to the Reverie + "ar" -> "#categories-7 ul li.cat-item-699200615" // Return to the Reverie + "it" -> "#categories-7 ul li.cat-item-699200615" // Return to the Reverie + else -> "#categories-7 ul li.cat-item-693784776, #categories-7 ul li.cat-item-699200615" // Chapter Secrets (multilingual), Return to the Reverie + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.title = element.select("a").text() + manga.setUrlWithoutDomain(element.select("a").attr("href")) + return manga + } + + override fun popularMangaNextPageSelector(): String? = null + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page) + + override fun latestUpdatesFromElement(element: Element): SManga = throw Exception("This method should not be called!") + + override fun latestUpdatesNextPageSelector(): String? = null + + // Search + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchMangaParse(response, query) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(1) + + private fun searchMangaParse(response: Response, query: String): MangasPage { + return MangasPage(popularMangaParse(response).mangas.filter { it.title.contains(query, ignoreCase = true) }, false) + } + + override fun searchMangaSelector() = throw UnsupportedOperationException("Not used") + + override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used") + + // Details + + override fun mangaDetailsParse(document: Document): SManga { + val manga = SManga.create() + manga.title = document.select("h1.page-title").text().replace("Category: ", "") + manga.thumbnail_url = chooseChapterThumbnail(document, manga.title) + manga.description = "" + manga.status = SManga.ONGOING + return manga + } + + // Use one of the chapter thumbnails as manga thumbnail + // Some thumbnails have a flag on them which indicates the Language. + // Try to choose a thumbnail with a matching flag + private fun chooseChapterThumbnail(document: Document, mangaTitle: String): String? { + var imgElement: Element? = null + + // Reverie + if (mangaTitle.contains("Reverie")) { + imgElement = document.select("article").firstOrNull { element -> + val chapterTitle = element.select("h2.entry-title a").text() + (chapterTitle.contains(siteLang) || (lang == "en" && !chapterTitle.contains(Regex("""(French|Arabic|Italian|Indonesia|Spanish)""")))) + } + } + // Chapter Secrets (multilingual) + if (mangaTitle.contains("Chapter Secrets") && lang != "en") { + imgElement = document.select("article").firstOrNull { + val chapterTitle = it.select("h2.entry-title a").text() + ((lang == "id" && chapterTitle.contains("Indonesia")) || (lang == "es" && !chapterTitle.contains("Indonesia"))) + } + } + + // Fallback + imgElement = imgElement ?: document.select("article:first-of-type").firstOrNull() + return imgElement?.select("img")?.attr("abs:src") + } + + // Chapters + + override fun chapterListSelector() = "article" + + override fun chapterFromElement(element: Element): SChapter { + val chapter = SChapter.create() + + chapter.setUrlWithoutDomain(element.select("a.entry-thumbnail").attr("href")) + chapter.name = element.select("h2.entry-title a").text() + chapter.date_upload = parseChapterDate(element.select("span.posted-on time").attr("datetime")) + + return chapter + } + + private fun parseChapterDate(date: String): Long { + val parsedDate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(date.replace("+00:00", "+0000")) + return parsedDate?.time ?: 0L + } + + private fun chapterNextPageSelector() = "div.nav-previous a" + + override fun chapterListParse(response: Response): List { + val allChapters = mutableListOf() + var document = response.asJsoup() + + while (true) { + val pageChapters = document.select(chapterListSelector()).map { chapterFromElement(it) } + if (pageChapters.isEmpty()) + break + + allChapters += pageChapters + + val hasNextPage = document.select(chapterNextPageSelector()).isNotEmpty() + if (!hasNextPage) + break + + val nextUrl = document.select(chapterNextPageSelector()).attr("href") + document = client.newCall(GET(nextUrl, headers)).execute().asJsoup() + } + + if (allChapters.isNotEmpty() && allChapters[0].name.contains("Reverie")) { + return when (lang) { + "fr" -> allChapters.filter { it.name.contains("French") }.toMutableList() + "ar" -> allChapters.filter { it.name.contains("Arabic") }.toMutableList() + "it" -> allChapters.filter { it.name.contains("Italian") }.toMutableList() + "id" -> allChapters.filter { it.name.contains("Indonesia") }.toMutableList() + "es" -> allChapters.filter { it.name.contains("Spanish") }.toMutableList() + else -> allChapters.filter { // english + !it.name.contains("French") && + !it.name.contains("Arabic") && + !it.name.contains("Italian") && + !it.name.contains("Indonesia") && + !it.name.contains("Spanish") }.toMutableList() + } + } + + // Remove Indonesian posts if lang is spanish + // Indonesian and Spanish posts are mixed in the same category "multilingual" on the website + // BTW, the same problem doesn't apply if lang is Indonesian because Indonesian has its own category + if (lang == "es") { + return allChapters.filter { !it.name.contains("Indonesia") }.toMutableList() + } + + return allChapters + } + + // Pages + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + document.select("div.entry-content").select("a img, img.size-full").forEachIndexed { i, img -> + pages.add(Page(i, "", img.attr("data-orig-file"))) + } + + return pages + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used") +} diff --git a/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOharaFactory.kt b/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOharaFactory.kt new file mode 100644 index 000000000..8f084dfe3 --- /dev/null +++ b/src/all/thelibraryofohara/src/eu/kanade/tachiyomi/extension/all/thelibraryofohara/TheLibraryOfOharaFactory.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.extension.all.thelibraryofohara + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class TheLibraryOfOharaFactory : SourceFactory { + override fun createSources(): List = languageList.map { TheLibraryOfOhara(it.tachiLang, it.siteLang) } +} + +private data class Source(val tachiLang: String, val siteLang: String) + +private val languageList = listOf( + + Source("id", "Indonesia"), + Source("en", "English"), + Source("es", "Spanish"), + Source("it", "Italian"), + Source("ar", "Arabic"), + Source("fr", "French") + +)