diff --git a/src/all/freleinbooks/AndroidManifest.xml b/src/all/freleinbooks/AndroidManifest.xml new file mode 100644 index 000000000..b4571bfa8 --- /dev/null +++ b/src/all/freleinbooks/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/all/freleinbooks/build.gradle b/src/all/freleinbooks/build.gradle new file mode 100644 index 000000000..de6902261 --- /dev/null +++ b/src/all/freleinbooks/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Frelein Books' + pkgNameSuffix = 'all.freleinbooks' + extClass = '.FreleinBooks' + extVersionCode = 1 + isNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/freleinbooks/res/mipmap-hdpi/ic_launcher.png b/src/all/freleinbooks/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..ba71baf35 Binary files /dev/null and b/src/all/freleinbooks/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/freleinbooks/res/mipmap-mdpi/ic_launcher.png b/src/all/freleinbooks/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..87f734066 Binary files /dev/null and b/src/all/freleinbooks/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/freleinbooks/res/mipmap-xhdpi/ic_launcher.png b/src/all/freleinbooks/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f0c2ab996 Binary files /dev/null and b/src/all/freleinbooks/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/freleinbooks/res/mipmap-xxhdpi/ic_launcher.png b/src/all/freleinbooks/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ca781e6e6 Binary files /dev/null and b/src/all/freleinbooks/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/freleinbooks/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/freleinbooks/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..770228b15 Binary files /dev/null and b/src/all/freleinbooks/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/freleinbooks/res/web_hi_res_512.png b/src/all/freleinbooks/res/web_hi_res_512.png new file mode 100644 index 000000000..302b4f9c1 Binary files /dev/null and b/src/all/freleinbooks/res/web_hi_res_512.png differ diff --git a/src/all/freleinbooks/src/eu/kanade/tachiyomi/extension/all/freleinbooks/FreleinBooks.kt b/src/all/freleinbooks/src/eu/kanade/tachiyomi/extension/all/freleinbooks/FreleinBooks.kt new file mode 100644 index 000000000..1559a2d88 --- /dev/null +++ b/src/all/freleinbooks/src/eu/kanade/tachiyomi/extension/all/freleinbooks/FreleinBooks.kt @@ -0,0 +1,271 @@ +package eu.kanade.tachiyomi.extension.all.freleinbooks + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +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 +import java.util.Calendar +import java.util.Locale + +class FreleinBooks() : ParsedHttpSource() { + override val baseUrl = "https://books.frelein.my.id" + override val lang = "all" + override val name = "Frelein Books" + override val supportsLatest = true + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val Element.imgSrc: String + get() = attr("data-lazy-src") + .ifEmpty { attr("data-src") } + .ifEmpty { attr("src") } + + // Latest + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.selectFirst("img")!!.imgSrc + manga.title = element.select(".postTitle").text() + manga.setUrlWithoutDomain(element.select(".postTitle > a").attr("abs:href")) + return manga + } + + override fun latestUpdatesNextPageSelector() = ".olderLink" + override fun latestUpdatesRequest(page: Int): Request { + return if (page == 1) { + GET(baseUrl) + } else { + val dateParam = page * 7 * 2 + // Calendar set to the current date + val calendar: Calendar = Calendar.getInstance() + // rollback 14 days + calendar.add(Calendar.DAY_OF_YEAR, -dateParam) + val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) + // now the date is 14 days back + GET("$baseUrl/search?updated-max=${formatter.format(calendar.time)}T12:38:00%2B07:00&max-results=12&start=12&by-date=false") + } + } + + override fun latestUpdatesSelector() = ".blogPosts > article" + + // Popular + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.thumbnail_url = element.selectFirst("img")!!.imgSrc + manga.title = element.select("h3").text() + manga.setUrlWithoutDomain(element.select("h3 > a").attr("abs:href")) + return manga + } + + override fun popularMangaNextPageSelector(): String? = null + override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page) + override fun popularMangaSelector() = ".itemPopulars article" + + // Search + override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val filterList = if (filters.isEmpty()) getFilterList() else filters + val tagFilter = filterList.findInstance()!! + val groupFilter = filterList.findInstance()!! + val magazineFilter = filterList.findInstance()!! + val fashionMagazineFilter = filterList.findInstance()!! + return when { + query.isEmpty() && groupFilter.state != 0 -> GET("$baseUrl/search/label/${groupFilter.toUriPart()}") + query.isEmpty() && magazineFilter.state != 0 -> GET("$baseUrl/search/label/${magazineFilter.toUriPart()}") + query.isEmpty() && fashionMagazineFilter.state != 0 -> GET("$baseUrl/search/label/${fashionMagazineFilter.toUriPart()}") + query.isEmpty() && tagFilter.state.isNotEmpty() -> GET("$baseUrl/search/label/${tagFilter.state}") + query.isNotEmpty() -> GET("$baseUrl/search?q=$query") + else -> latestUpdatesRequest(page) + } + } + + override fun searchMangaSelector() = latestUpdatesSelector() + + // Details + override fun mangaDetailsParse(document: Document): SManga { + val manga = SManga.create() + manga.title = document.select(".postTitle").text() + manga.description = "Read ${document.select(".postTitle").text()} \n \nNote: If you encounters error when opening the magazine, please press the WebView button then leave a comment on our web so we can update it soon." + manga.genre = document.select(".labelLink > a") + .joinToString(", ") { it.text() } + manga.status = SManga.COMPLETED + return manga + } + + override fun chapterFromElement(element: Element): SChapter { + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href")) + chapter.name = "Gallery" + chapter.date_upload = getDate(element.select("link[rel=\"canonical\"]").attr("href")) + return chapter + } + + override fun chapterListSelector() = "html" + + // Pages + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + document.select("noscript").remove() + document.select(".gallerybox a > img").forEachIndexed { i, it -> + // format new img/b/ + if (it.imgSrc.contains("img/b/")) { + if (it.imgSrc.contains("/w768-rw/")) { + val itUrl = it.imgSrc.replace("/w768-rw/", "/s0/") + pages.add(Page(i, itUrl, itUrl)) + } + if (it.imgSrc.contains("/w480-rw/")) { + val itUrl = it.imgSrc.replace("/w480-rw/", "/s0/") + pages.add(Page(i, itUrl, itUrl)) + } + } + // format new img/b/ + else { + if (it.imgSrc.contains("=w768-rw")) { + val itUrl = it.imgSrc.replace("=w768-rw", "") + pages.add(Page(i, itUrl, itUrl)) + } else if (it.imgSrc.contains("=w480-rw")) { + val itUrl = it.imgSrc.replace("=w480-rw", "") + pages.add(Page(i, itUrl, itUrl)) + } else { + val itUrl = it.imgSrc + pages.add(Page(i, itUrl, itUrl)) + } + } + } + return pages + } + + override fun imageUrlParse(document: Document): String = + throw UnsupportedOperationException("Not used") + + // Filters + + override fun getFilterList(): FilterList = FilterList( + Filter.Header("NOTE: Only one filter will be applied!"), + Filter.Separator(), + GroupFilter(), + MagazineFilter(), + FashionMagazineFilter(), + TagFilter(), + ) + + open class UriPartFilter( + displayName: String, + private val valuePair: Array>, + ) : Filter.Select(displayName, valuePair.map { it.first }.toTypedArray()) { + fun toUriPart() = valuePair[state].second + } + + class MagazineFilter : UriPartFilter( + "Magazine", + arrayOf( + Pair("Any", ""), + Pair("B.L.T.", "B.L.T."), + Pair("BIG ONE GIRLS", "BIG ONE GIRLS"), + Pair("BOMB!", "BOMB!"), + Pair("BRODY", "BRODY"), + Pair("BUBKA", "BUBKA"), + Pair("ENTAME", "ENTAME"), + Pair("EX Taishu", "EX Taishu"), + Pair("FINEBOYS", "FINEBOYS"), + Pair("FLASH", "FLASH"), + Pair("Fine", "Fine"), + Pair("Friday", "Friday"), + Pair("HINA_SATSU", "HINA_SATSU"), + Pair("IDOL AND READ", "IDOL AND READ"), + Pair("Kadokawa Scene 07", "Kadokawa Scene 07"), + Pair("Monthly Basketball", "Monthly Basketball"), + Pair("Monthly Young Magazine", "Monthly Young Magazine"), + Pair("NOGI_SATSU", "NOGI_SATSU"), + Pair("Nylon Japan", "Nylon Japan"), + Pair("Platinum FLASH", "Platinum FLASH"), + Pair("Shonen Magazine", "Shonen Magazine"), + Pair("Shukan Post", "Shukan Post"), + Pair("TOKYO NEWS MOOK", "TOKYO NEWS MOOK"), + Pair("TV LIFE,Tarzan", "TV LIFE,Tarzan"), + Pair("Tokyo Calendar", "Tokyo Calendar"), + Pair("Top Yell NEO", "Top Yell NEO"), + Pair("UTB", "UTB"), + Pair("Weekly Playboy", "Weekly Playboy"), + Pair("Weekly SPA", "Weekly SPA"), + Pair("Weekly SPA!", "Weekly SPA!"), + Pair("Weekly Shonen Champion", "Weekly Shonen Champion"), + Pair("Weekly Shonen Magazine", "Weekly Shonen Magazine"), + Pair("Weekly Shonen Sunday", "Weekly Shonen Sunday"), + Pair("Weekly Shounen Magazine", "Weekly Shounen Magazine"), + Pair("Weekly The Television Plus", "Weekly The Television Plus"), + Pair("Weekly Zero Jump", "Weekly Zero Jump"), + Pair("Yanmaga Web", "Yanmaga Web"), + Pair("Young Animal", "Young Animal"), + Pair("Young Champion", "Young Champion"), + Pair("Young Gangan", "Young Gangan"), + Pair("Young Jump", "Young Jump"), + Pair("Young Magazine", "Young Magazine"), + Pair("blt graph.", "blt graph."), + Pair("mini", "mini"), + ), + ) + + class FashionMagazineFilter : UriPartFilter( + "Fashion Magazine", + arrayOf( + Pair("Any", ""), + Pair("BAILA", "BAILA"), + Pair("Biteki", "Biteki"), + Pair("CLASSY", "CLASSY"), + Pair("CanCam", "CanCam"), + Pair("JJ", "JJ"), + Pair("LARME", "LARME"), + Pair("MARQUEE", "MARQUEE"), + Pair("Maquia", "Maquia"), + Pair("Men's non-no", "Men's non-no"), + Pair("More", "More"), + Pair("Oggi", "Oggi"), + Pair("Ray", "Ray"), + Pair("Seventeen", "Seventeen"), + Pair("Sweet", "Sweet"), + Pair("VOCE", "VOCE"), + Pair("ViVi", "ViVi"), + Pair("With", "With"), + Pair("aR", "aR"), + Pair("anan", "anan"), + Pair("bis", "bis"), + Pair("non-no", "non-no"), + ), + ) + + class GroupFilter : UriPartFilter( + "Group", + arrayOf( + Pair("Any", ""), + Pair("Hinatazaka46", "Hinatazaka46"), + Pair("Nogizaka46", "Nogizaka46"), + Pair("Sakurazaka46", "Sakurazaka46"), + Pair("Keyakizaka46", "Keyakizaka46"), + ), + ) + + class TagFilter : Filter.Text("Tag") + + private inline fun Iterable<*>.findInstance() = find { it is T } as? T + + private fun getDate(str: String): Long { + val regex = "[0-9]{4}\\/[0-9]{2}\\/[0-9]{2}".toRegex() + val match = regex.find(str) + return runCatching { DATE_FORMAT.parse(match!!.value)?.time }.getOrNull() ?: 0L + } + + companion object { + private val DATE_FORMAT by lazy { + SimpleDateFormat("yyyy/MM/dd", Locale.US) + } + } +}