diff --git a/src/en/schlockmercenary/build.gradle b/src/en/schlockmercenary/build.gradle new file mode 100644 index 000000000..0310d312f --- /dev/null +++ b/src/en/schlockmercenary/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Schlock Mercenary' + pkgNameSuffix = 'en.schlockmercenary' + extClass = '.Schlockmercenary' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/schlockmercenary/res/mipmap-hdpi/ic_launcher.png b/src/en/schlockmercenary/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a9da83bf4 Binary files /dev/null and b/src/en/schlockmercenary/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/schlockmercenary/res/mipmap-mdpi/ic_launcher.png b/src/en/schlockmercenary/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..051be4d1b Binary files /dev/null and b/src/en/schlockmercenary/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/schlockmercenary/res/mipmap-xhdpi/ic_launcher.png b/src/en/schlockmercenary/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a59e9a799 Binary files /dev/null and b/src/en/schlockmercenary/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/schlockmercenary/res/mipmap-xxhdpi/ic_launcher.png b/src/en/schlockmercenary/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..c6bd4d2f9 Binary files /dev/null and b/src/en/schlockmercenary/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/schlockmercenary/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/schlockmercenary/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..7f09dd789 Binary files /dev/null and b/src/en/schlockmercenary/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/schlockmercenary/res/web_hi_res_512.png b/src/en/schlockmercenary/res/web_hi_res_512.png new file mode 100644 index 000000000..3cf4c70b7 Binary files /dev/null and b/src/en/schlockmercenary/res/web_hi_res_512.png differ diff --git a/src/en/schlockmercenary/src/eu/kanade/tachiyomi/extension/en/schlockmercenary/Schlockmercenary.kt b/src/en/schlockmercenary/src/eu/kanade/tachiyomi/extension/en/schlockmercenary/Schlockmercenary.kt new file mode 100644 index 000000000..ec0e37ecf --- /dev/null +++ b/src/en/schlockmercenary/src/eu/kanade/tachiyomi/extension/en/schlockmercenary/Schlockmercenary.kt @@ -0,0 +1,187 @@ +package eu.kanade.tachiyomi.extension.en.schlockmercenary + +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.lang.UnsupportedOperationException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar +import java.util.Locale +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class Schlockmercenary : ParsedHttpSource() { + + override val name = "Schlock Mercenary" + + override val baseUrl = "https://www.schlockmercenary.com" + + override val lang = "en" + + override val supportsLatest = false + + private var chapterCount = 1 + + // Books + + override fun popularMangaRequest(page: Int): Request = GET("${baseUrl}$archiveUrl") + + override fun popularMangaSelector(): String = "div.archive-book" + + override fun popularMangaFromElement(element: Element): SManga { + val book = element.select("h4 > a").first() + val thumb = (baseUrl + (element.select("img").first()?.attr("src") + ?: defaultThumbnailUrl)).substringBefore("?") + return SManga.create().apply { + url = book.attr("href") + title = book.text() + artist = "Howard Tayler" + author = "Howard Tayler" + // Schlock Mercenary finished as of July 2020 + status = SManga.COMPLETED + description = element.select("p").first()?.text() ?: "" + thumbnail_url = thumb + } + } + + // Chapters + + override fun chapterListSelector() = "ul.chapters > li:not(ul > li > ul > li) > a" + + override fun fetchChapterList(manga: SManga): Observable> { + val requestUrl = "${baseUrl}$archiveUrl" + return client.newCall(GET(requestUrl)) + .asObservableSuccess() + .map { response -> + getChaptersForBook(response, manga) + } + } + + private fun getChaptersForBook(response: Response, manga: SManga): List { + val document = response.asJsoup() + val sanitizedTitle = manga.title.replace("""([",'])""".toRegex(), "\\\\$1") + val book = document.select(popularMangaSelector() + ":contains($sanitizedTitle)") + val chapters = mutableListOf() + chapterCount = 1 + book.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) } + return chapters + } + + override fun chapterFromElement(element: Element): SChapter { + val chapter = SChapter.create() + chapter.url = element.attr("href") + chapter.name = element.text() + chapter.chapter_number = chapterCount++.toFloat() + chapter.date_upload = chapter.url.takeLast(10).let { + SimpleDateFormat(dateFormat, Locale.getDefault()).parse(it)!!.time + } + return chapter + } + + // Pages + + override fun fetchPageList(chapter: SChapter): Observable> { + val requestUrl = "${baseUrl}$archiveUrl" + return client.newCall(GET(requestUrl)) + .asObservableSuccess() + .map { response -> + getPagesForChapter(response, chapter) + } + } + + private fun getPagesForChapter(response: Response, chapter: SChapter): List { + val document = response.asJsoup() + + /** + * To find the end page, first, the next chapter start must be found, + * then subtract one day off of the next chapter start. + * If no chapter start is found, grab the next book start. + * If no next book exists, assume one page long. + */ + val currentChapter = document.select(chapterListSelector() + "[href=${chapter.url}]").first()!! + val start = chapterFromElement(currentChapter).date_upload + // Find next chapter start + var nextChapter = currentChapter.parent()?.nextElementSibling()?.select("a")?.first() + var end = start + 1 + // Chapter is the last in the book + + if (nextChapter == null) { + // Grab next book start. + nextChapter = currentChapter.parents()[2]?.nextElementSibling()?.select(chapterListSelector())?.first() + } + + if (nextChapter != null) { + end = chapterFromElement(nextChapter).date_upload + } + + return generatePageListBetweenDates(start, end) + } + + private fun generatePageListBetweenDates(start: Long, end: Long): List { + val pages = mutableListOf() + val calendar = GregorianCalendar() + calendar.time = Date(start) + + while (calendar.time.before(Date(end))) { + val result = calendar.time + val formatter = SimpleDateFormat(dateFormat, Locale.getDefault()) + val today = formatter.format(result) + getImageUrlsForDay(today).forEach { pages.add(Page(pages.size, "", it)) } + calendar.add(Calendar.DATE, 1) + } + + return pages + } + + private fun getImageUrlsForDay(day: String): List { + val requestUrl = "$baseUrl/$day" + val document = client.newCall(GET(requestUrl)).execute().asJsoup() + val urls = document.select("div#strip-$day > img").map { it.attr("abs:src") } + return urls + } + + override fun fetchMangaDetails(manga: SManga): Observable = Observable.just(manga) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.just(MangasPage(emptyList(), false)) + + override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException("Not used") + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used") + + override fun popularMangaNextPageSelector(): String? = null + + override fun searchMangaSelector(): String = throw UnsupportedOperationException("Not used") + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used") + + override fun mangaDetailsParse(document: Document): SManga = throw UnsupportedOperationException("Not used") + + override fun pageListParse(document: Document): List = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesSelector(): String = throw UnsupportedOperationException("Not used") + + companion object { + const val defaultThumbnailUrl = "/static/img/logo.b6dacbb8.jpg" + const val archiveUrl = "/archives/" + const val dateFormat = "yyyy-MM-dd" + } +}