diff --git a/src/fr/scantradunion/build.gradle b/src/fr/scantradunion/build.gradle new file mode 100644 index 000000000..1519d1e5e --- /dev/null +++ b/src/fr/scantradunion/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Scantrad Union' + pkgNameSuffix = 'fr.scantradunion' + extClass = '.ScantradUnion' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/fr/scantradunion/res/mipmap-hdpi/ic_launcher.png b/src/fr/scantradunion/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..ed532ebd7 Binary files /dev/null and b/src/fr/scantradunion/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/fr/scantradunion/res/mipmap-mdpi/ic_launcher.png b/src/fr/scantradunion/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c6d9278c8 Binary files /dev/null and b/src/fr/scantradunion/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/fr/scantradunion/res/mipmap-xhdpi/ic_launcher.png b/src/fr/scantradunion/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2c1943cf4 Binary files /dev/null and b/src/fr/scantradunion/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/fr/scantradunion/res/mipmap-xxhdpi/ic_launcher.png b/src/fr/scantradunion/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..199390624 Binary files /dev/null and b/src/fr/scantradunion/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/fr/scantradunion/res/mipmap-xxxhdpi/ic_launcher.png b/src/fr/scantradunion/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f2c2802f1 Binary files /dev/null and b/src/fr/scantradunion/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/fr/scantradunion/res/web_hi_res_512.png b/src/fr/scantradunion/res/web_hi_res_512.png new file mode 100644 index 000000000..f10def9b7 Binary files /dev/null and b/src/fr/scantradunion/res/web_hi_res_512.png differ diff --git a/src/fr/scantradunion/src/eu/kanade/tachiyomi/extension/fr/scantradunion/ScantradUnion.kt b/src/fr/scantradunion/src/eu/kanade/tachiyomi/extension/fr/scantradunion/ScantradUnion.kt new file mode 100644 index 000000000..0160ff5bb --- /dev/null +++ b/src/fr/scantradunion/src/eu/kanade/tachiyomi/extension/fr/scantradunion/ScantradUnion.kt @@ -0,0 +1,165 @@ +package eu.kanade.tachiyomi.extension.fr.scantradunion + +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 java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class ScantradUnion : ParsedHttpSource() { + override val name = "Scantrad Union" + override val baseUrl = "https://scantrad-union.com" + override val lang = "fr" + override val supportsLatest = true + + // If these parameters are not used, the search results are incomplete. + private val searchUrlSuffix = "&asp_active=1&p_asid=1&p_asp_data=YXNwX2dlbiU1QiU1RD10aXRsZSZjdXN0b21zZXQlNUIlNUQ9bWFuZ2E=" + private val frenchDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.FRANCE) + + override fun chapterFromElement(element: Element): SChapter { + val chapterNumberStr = element.select(".chapter-number").text() + // We don't have a css selector to select the date directly, but we know that it will always + // be the third child of a .name-chapter. + val dateUploadStr = element.select(".name-chapter").first().children().elementAt(2).text() + val chapterName = element.select(".chapter-name").text() + // The only way to get the chapter url is to check all .btnlel and take the one starting with https://... + val url = element.select(".btnlel").map { it.attr("href") }.first { it.startsWith("https://scantrad-union.com/read/") } + val chapterNumberStrFormatted = formatMangaNumber(chapterNumberStr) + + val chapter = SChapter.create() + // The chapter name is often empty + // So we will display ${chapterNumber} - ${chapterName} and only ${chapterNumber} if chapterName is empty + chapter.name = listOf(chapterNumberStrFormatted, chapterName).filter(String::isNotBlank).joinToString(" - ") + chapter.date_upload = parseFrenchDateFromString(dateUploadStr) + // The scanlator is several teams of translators concatenated in one string. + chapter.scanlator = element.select(".btnteam").joinToString(" ") { teamElem -> teamElem.text() } + chapter.setUrlWithoutDomain(url) + return chapter + } + + override fun chapterListSelector(): String = ".links-projects li" + + override fun imageUrlParse(document: Document): String = "" + + override fun latestUpdatesFromElement(element: Element): SManga { + val title = element.select("a.text-truncate").text() + val url = element.select("a.text-truncate").attr("href") + + val manga = SManga.create() + manga.title = formatMangaTitle(title) + manga.thumbnail_url = element.select("img.attachment-thumbnail").attr("src") + manga.author = element.select(".nomteam").text() + // Cannot distinguish authors and artists because they are in the same section. + manga.artist = manga.author + manga.setUrlWithoutDomain(url) + return manga + } + + override fun latestUpdatesNextPageSelector(): String? = null + + override fun latestUpdatesRequest(page: Int): Request { + return GET(baseUrl, headers) + } + + override fun latestUpdatesSelector() = ".dernieresmaj .colonne" + + override fun mangaDetailsParse(document: Document): SManga { + val title = document.select(".projet-description h2").text() + val statusStr = document.select(".label.label-primary")[2].text() + + val manga = SManga.create() + manga.title = formatMangaTitle(title) + manga.thumbnail_url = document.select(".projet-image img").attr("src") + manga.description = document.select(".sContent").text() + manga.author = document.select("div.project-details a[href*=auteur]") + .joinToString(", ") { teamElem -> teamElem.text() } + // Cannot distinguish authors and artists because they are in the same section. + manga.artist = manga.author + manga.status = mapMangaStatusStringToConst(statusStr) + manga.setUrlWithoutDomain(document.location()) + return manga + } + + override fun pageListParse(document: Document): List { + return document.select("#webtoon a img") + .mapIndexed { index: Int, imgElem: Element -> + // In webtoon mode, images have an src attribute only. + // In manga mode, images have a data-src attribute that contains the src + val imgElemDataSrc = imgElem.attr("data-src") + val imgElemSrc = imgElem.attr("src") + + val imgUrl = if (imgElemDataSrc.isNullOrBlank()) imgElemSrc else imgElemDataSrc + + Page(index, "", imgUrl) + } + } + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.title = formatMangaTitle(element.select(".index-top3-title").text()) + manga.setUrlWithoutDomain(element.attr("href")) + manga.thumbnail_url = element.select(".index-top3-bg").attr("style") + .substringAfter("background:url('").substringBefore("')") + return manga + } + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/projets/", headers) + } + + override fun popularMangaNextPageSelector(): String? = null + + override fun popularMangaSelector(): String = ".index-top3-a" + + override fun searchMangaFromElement(element: Element): SManga { + val titleLinkElem = element.select("a.index-post-header-a") + + val manga = SManga.create() + manga.title = formatMangaTitle(titleLinkElem.text()) + manga.setUrlWithoutDomain(titleLinkElem.attr("href")) + manga.thumbnail_url = element.select("img.wp-post-image").attr("src") + return manga + } + + override fun searchMangaNextPageSelector(): String? = null + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val searchUrl = "$baseUrl/?s=$query$searchUrlSuffix" + return GET(searchUrl, headers) + } + + override fun searchMangaSelector(): String = "article.post-outer" + + private fun formatMangaNumber(value: String): String { + return value.removePrefix("#").trim() + } + + private fun formatMangaTitle(value: String): String { + // Translations produced by Scantrad Union partners are prefixed with "[Partenaire] ". + return value.removePrefix("[Partenaire]").trim() + } + + private fun parseFrenchDateFromString(value: String): Long { + return try { + frenchDateFormat.parse(value)?.time ?: 0L + } catch (ex: ParseException) { + 0L + } + } + + private fun mapMangaStatusStringToConst(status: String): Int { + return when (status.trim().toLowerCase(Locale.FRENCH)) { + "en cours" -> SManga.ONGOING + "terminé" -> SManga.COMPLETED + "licencié" -> SManga.LICENSED + else -> SManga.UNKNOWN + } + } +}