Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'AnimeSama'
pkgNameSuffix = 'fr.animesama'
extClass = '.AnimeSama'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,171 @@
package eu.kanade.tachiyomi.extension.fr.animesama
import android.net.Uri
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
class AnimeSama : ParsedHttpSource() {
override val name = "AnimeSama"
override val baseUrl = "https://anime-sama.fr"
val cdn_url = "https://cdn.statically.io/gh/Anime-Sama/IMG/img/animes/animes%20icones%20carr%C3%A9/"
override val lang = "fr"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Accept-Language", "fr-FR")
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/scan", headers)
}
override fun popularMangaSelector() = "figure.figure"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("figcaption").text()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = element.select("a > img").attr("src")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun latestUpdatesSelector() = "div.container-fluid:nth-child(15) > div:nth-child(1) figure"
override fun latestUpdatesFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("figcaption").text().replace("\\nScan\\n", "")
setUrlWithoutDomain(cdn_url + title.replace(" ", "-").trim() + "carre.jpg")
thumbnail_url = element.select("img").attr("src")
}
}
override fun latestUpdatesNextPageSelector(): String? = null
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/search/search.php").buildUpon()
.appendQueryParameter("terme", query + " [SCANS]")
.appendQueryParameter("s", "Search")
return GET(uri.toString(), headers)
}
override fun searchMangaSelector() = "div.media-body"
override fun searchMangaNextPageSelector(): String? = null
override fun searchMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("h5").text()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url =
cdn_url + title.replace(
" [SCANS]",
"",
).replace(" ", "-").trim() + "carre.jpg"
}
}
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select("div.carousel-item:nth-child(1) > div:nth-child(2) > h5:nth-child(1)").text()
description = document.select("div.carousel-item:nth-child(2) > div:nth-child(2) > p:nth-child(1)").text()
thumbnail_url = document.select("div.carousel-item:nth-child(1) > img:nth-child(1)").attr("src")
genre = document.select("div.carousel-item:nth-child(2) > div:nth-child(2) > p:nth-child(2)").text().replace("Genres : ", "")
}
// Chapters
override fun chapterListSelector() = throw Exception("Not used")
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val javascriptUrl = document.select("body > script:nth-child(3)").attr("abs:src")
val newHeaders = headersBuilder()
.add("Accept-Version", "v1")
.build()
val request = GET(javascriptUrl, newHeaders)
val responsejs = client.newCall(request).execute()
val jsonDataString = responsejs.body.string()
return jsonDataString
.split(" ", ",")
.filter { it.contains("eps") && !it.contains("drive.google.com") }
.mapNotNull { it.replace("=", "").replace("eps", "").toIntOrNull() }
.sorted()
.map { chapter ->
SChapter.create().apply {
name = "Chapitre $chapter"
setUrlWithoutDomain(javascriptUrl + "/$chapter")
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val url = document.baseUri().split("/")
val javascriptUrlFinal = url.subList(0, url.size - 1).joinToString("/")
val javascriptResponse = checkJavascript(javascriptUrlFinal)
val jsonDataString = javascriptResponse.body.string()
val episode = url[url.size - 1]
val allEpisodes = jsonDataString.split("var")
val theEpisode = allEpisodes.firstOrNull { it.contains("eps$episode") }
?: return emptyList()
val final_list = theEpisode
.substringAfter("[")
.substringBefore("]")
return final_list
.substring(0, final_list.lastIndexOf(",")).replace("""'""".toRegex(), "\"")
.let { json.decodeFromString<List<String>>("[$it]") }
.mapIndexed { i, imageUrl -> Page(i, "", imageUrl) }
}
private fun checkJavascript(url: String): Response {
val request = GET(url, headers)
return client.newCall(request).execute()
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder()
.add("Referer", baseUrl)
.build()
return GET(page.imageUrl!!, imgHeaders)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'AralosBD'
pkgNameSuffix = 'fr.aralosbd'
extClass = '.AralosBD'
extVersionCode = 6
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1,264 @@
package eu.kanade.tachiyomi.extension.fr.aralosbd
import eu.kanade.tachiyomi.network.GET
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.HttpSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import org.jsoup.parser.Parser
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class AralosBD : HttpSource() {
companion object {
val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.FRANCE)
val LINK_REGEX = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
val BOLD_REGEX = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
val ITALIC_REGEX = "_+\\s*([^_]*)\\s*_+".toRegex()
val ICON_REGEX = ":+[a-zA-Z]+:".toRegex()
val PAGE_REGEX = "page:([0-9]+)".toRegex()
}
private fun cleanString(string: String): String {
return Parser.unescapeEntities(string, false)
.substringBefore("---")
.replace(LINK_REGEX, "$2")
.replace(BOLD_REGEX, "$1")
.replace(ITALIC_REGEX, "$1")
.replace(ICON_REGEX, "")
.trim()
}
override val name = "AralosBD"
override val baseUrl = "https://aralosbd.fr"
override val lang = "fr"
override val supportsLatest = true
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request {
// Let's use a request that just query everything by page, sorted by total views (title + chapters)
return GET("$baseUrl/manga/search?s=sort:allviews;limit:24;-id:3;page:${page - 1};order:desc", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val searchResult = json.decodeFromString<AralosBDSearchResult>(response.body.string())
var hasNextPage = false
val pageMatch = PAGE_REGEX.find(response.request.url.toString())
if (pageMatch != null && pageMatch.groupValues.count() > 1) {
val currentPage = pageMatch.groupValues[1].toInt()
hasNextPage = currentPage < searchResult.page_count - 1
}
return MangasPage(searchResult.mangas.map(::searchMangaToSManga), hasNextPage)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val searchResult = json.decodeFromString<AralosBDSearchResult>(response.body.string())
var hasNextPage = false
val pageMatch = PAGE_REGEX.find(response.request.url.toString())
if (pageMatch != null && pageMatch.groupValues.count() > 1) {
val currentPage = pageMatch.groupValues[1].toInt()
hasNextPage = currentPage < searchResult.page_count - 1
}
return MangasPage(searchResult.mangas.map(::searchMangaToSManga), hasNextPage)
}
override fun latestUpdatesRequest(page: Int): Request {
// That's almost exactly the same stuff that the popular request, simply ordered differently
// A new title will always have a greater ID, so we can sort by ID. Using year would not be
// accurate because it's the release year
// It would be better to sort by last updated, but this is not yet in the API
return GET("$baseUrl/manga/search?s=sort:id;limit:24;-id:3;page:${page - 1};order:desc", headers)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// For a basic search, we call the appropriate endpoint
return GET("$baseUrl/manga/search?s=page:${page - 1};sort:id;order:desc;text:$query", headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val searchResult = json.decodeFromString<AralosBDSearchResult>(response.body.string())
var hasNextPage = false
val pageMatch = PAGE_REGEX.find(response.request.url.toString())
if (pageMatch != null && pageMatch.groupValues.count() > 1) {
val currentPage = pageMatch.groupValues[1].toInt()
hasNextPage = currentPage < searchResult.page_count - 1
}
return MangasPage(searchResult.mangas.map(::searchMangaToSManga), hasNextPage)
}
override fun mangaDetailsRequest(manga: SManga): Request {
// This is a needed call, so the Tachiyomi user behave like a regular user
// return GET(manga.url.replace("display?", "api?get=manga&"), headers)
return GET(manga.url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val responseBody = client.newCall(GET(response.request.url.toString().replace("display?", "api?get=manga&"), headers)).execute().body
val manga = json.decodeFromString<AralosBDManga>(responseBody.string())
return SManga.create().apply {
url = "$baseUrl/manga/display?id=${manga.id}"
title = manga.main_title
artist = "" // manga.authors.joinToString(", ")
author = manga.authors?.joinToString(", ", transform = ::authorToString)
description = cleanString("${manga.description}\n\n" + (manga.fulldescription ?: ""))
status = 0 // This is not on the website
genre = manga.tags?.joinToString(", ", transform = ::tagToString)
thumbnail_url = "$baseUrl/${manga.icon}"
}
}
override fun chapterListRequest(manga: SManga): Request {
return GET(manga.url.replace("display?id", "api?get=chapters&manga"), headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val searchResult = json.decodeFromString<List<AralosBDChapter>>(response.body.string())
val validSearchResults = mutableListOf<AralosBDChapter>()
searchResult.filterTo(validSearchResults) { it.chapter_released == "1" }
return validSearchResults.map(::chapterToSChapter)
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(chapter.url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val responseBody = client.newCall(GET(response.request.url.toString().replace("chapter?id", "api?get=pages&chapter"), headers)).execute().body
val pageResult = json.decodeFromString<AralosBDPages>(responseBody.string())
return pageResult.links.mapIndexed { index, link ->
Page(
index,
"$baseUrl/$link",
"$baseUrl/$link",
)
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
private fun authorToString(author: AralosBDAuthor) = author.name
private fun tagToString(tag: AralosBDTag) = tag.tag
private fun searchMangaToSManga(manga: AralosBDSearchManga): SManga {
return SManga.create().apply {
// No need to trim, it's already done by the server
title = manga.title
// Just need to append the base url to the relative link returned
thumbnail_url = "$baseUrl/${manga.icon}"
// The url of the manga is simply based on the manga ID for now
url = "$baseUrl/manga/display?id=${manga.id}"
}
}
private fun chapterToSChapter(chapter: AralosBDChapter): SChapter {
return SChapter.create().apply {
url = "$baseUrl/manga/chapter?id=${chapter.chapter_id}"
name = chapter.chapter_number + " - " + chapter.chapter_title
date_upload = try { DATE_FORMAT.parse(chapter.chapter_release_time)!!.time } catch (e: Exception) { System.currentTimeMillis() }
// chapter_number = // This is a string and it can be 2.5.1 for example
scanlator = chapter.chapter_translator
}
}
}
@Serializable
data class AralosBDSearchManga(
val icon: String = "",
val title: String = "",
val id: String = "",
val read_count: String = "",
val chapter_count: String = "",
val is_favorite: Boolean = false,
val is_liked: Boolean = false,
)
@Serializable
data class AralosBDSearchResult(
val error: Int = 0,
val result_count: Int = 0,
val page_count: Int = 0,
val mangas: List<AralosBDSearchManga> = emptyList(),
)
@Serializable
data class AralosBDAlternativeTitle(
val title: String = "",
)
@Serializable
data class AralosBDAuthor(
val name: String = "",
)
@Serializable
data class AralosBDTranslator(
val name: String = "",
)
@Serializable
data class AralosBDTag(
val tag: String = "",
val color: String = "",
)
@Serializable
data class AralosBDManga(
val main_title: String = "",
val fulldescription: String? = "",
val description: String = "",
val year: String = "",
val id: Int = 0,
val alternative_titles: List<AralosBDAlternativeTitle>? = emptyList(),
val authors: List<AralosBDAuthor>? = emptyList(),
val translators: List<AralosBDTranslator>? = emptyList(),
val tags: List<AralosBDTag>? = emptyList(),
val banner: String = "",
val icon: String = "",
val error: Int = 0,
)
@Serializable
data class AralosBDChapter(
val chapter_number: String = "",
val chapter_user: String = "",
val chapter_title: String = "",
val chapter_translator: String? = "",
val chapter_view_count: String = "",
val chapter_like_count: String = "",
val chapter_date: String = "",
val chapter_id: String = "",
val chapter_read: Boolean = false,
val chapter_released: String = "0",
val chapter_release_time: String = "",
)
@Serializable
data class AralosBDPages(
val error: Int = 0,
val links: List<String> = emptyList(),
)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'FMTEAM'
pkgNameSuffix = 'fr.fmteam'
extClass = '.FMTEAM'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,129 @@
package eu.kanade.tachiyomi.extension.fr.fmteam
import eu.kanade.tachiyomi.network.GET
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class FMTEAM : HttpSource() {
override val name = "FMTEAM"
override val baseUrl = "https://fmteam.fr"
override val lang = "fr"
override val supportsLatest = true
override val versionId = 2
private val json: Json by injectLazy()
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
}
// All manga
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/api/comics")
override fun popularMangaParse(response: Response): MangasPage {
val results = json.decodeFromString<FmteamComicListPage>(response.body.string())
return MangasPage(results.comics.sortedByDescending { it.views }.map(::fmTeamComicToSManga), false)
}
// Latest
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/api/comics")
override fun latestUpdatesParse(response: Response): MangasPage {
val results = json.decodeFromString<FmteamComicListPage>(response.body.string())
return MangasPage(results.comics.sortedByDescending { parseDate(it.last_chapter.published_on) }.map(::fmTeamComicToSManga), false)
}
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/api/search/$query")
override fun searchMangaParse(response: Response): MangasPage {
val results = json.decodeFromString<FmteamComicListPage>(response.body.string())
return MangasPage(results.comics.map(::fmTeamComicToSManga), false)
}
// Manga details
override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl/api${manga.url}")
override fun mangaDetailsParse(response: Response): SManga {
val results = json.decodeFromString<FmteamComicDetailPage>(response.body.string())
return fmTeamComicToSManga(results.comic)
}
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl${manga.url}"
}
// Chapter list
override fun chapterListRequest(manga: SManga): Request = GET("$baseUrl/api${manga.url}")
override fun chapterListParse(response: Response): List<SChapter> {
val results = json.decodeFromString<FmteamComicDetailPage>(response.body.string())
return results.comic.chapters?.map(::fmTeamChapterToSChapter) ?: emptyList()
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
// Pages
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/api${chapter.url}")
override fun pageListParse(response: Response): List<Page> {
val results = json.decodeFromString<FmteamChapterDetailPage>(response.body.string())
return results.chapter.pages.orEmpty()
.mapIndexed { i, page -> Page(i, "${results.chapter.url}#${i + 1}", page) }
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used.")
// Utils
private fun fmTeamComicToSManga(comic: FmteamComic): SManga = SManga.create().apply {
url = comic.url
title = comic.title
artist = comic.artist
author = comic.author
description = comic.description
genre = comic.genres.joinToString { it.name }
status = when (comic.status) {
"En cours" -> SManga.ONGOING
"Termin\u00e9" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = comic.thumbnail
initialized = true
}
private fun fmTeamChapterToSChapter(chapter: FmteamChapter): SChapter = SChapter.create().apply {
url = chapter.url
name = chapter.full_title
date_upload = parseDate(chapter.published_on)
scanlator = chapter.teams.filterNotNull().joinToString { it.name }
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
}

View File

@@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.extension.fr.fmteam
import kotlinx.serialization.Serializable
@Serializable
data class FmteamComicListPage(
val comics: List<FmteamComic>,
)
@Serializable
data class FmteamComicDetailPage(
val comic: FmteamComic,
)
@Serializable
data class FmteamChapterDetailPage(
val chapter: FmteamChapter,
)
@Serializable
data class FmteamComic(
val title: String,
val thumbnail: String,
val description: String,
val author: String,
val artist: String?,
val genres: List<FmteamTag>,
val status: String,
val views: Int,
val url: String,
val last_chapter: FmteamChapter,
val chapters: List<FmteamChapter>?,
)
@Serializable
data class FmteamTag(
val name: String,
)
@Serializable
data class FmteamChapter(
val full_title: String,
val chapter: Int,
val subchapter: Int?,
val teams: List<FmteamTeam?>,
val published_on: String,
val url: String,
val pages: List<String>?,
)
@Serializable
data class FmteamTeam(
val name: String,
)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'FuryoSquad'
pkgNameSuffix = 'fr.furyosquad'
extClass = '.FuryoSquad'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -0,0 +1,252 @@
package eu.kanade.tachiyomi.extension.fr.furyosquad
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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 okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
class FuryoSquad : ParsedHttpSource() {
override val name = "FuryoSquad"
override val baseUrl = "https://www.furyosociety.com/"
override val lang = "fr"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(1)
.build()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/mangas", headers)
}
override fun popularMangaSelector() = "div#fs-tous div.fs-card-body"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
with(element) {
manga.url = select("div.fs-card-img-container a").attr("href")
manga.title = select("span.fs-comic-title a").text()
manga.thumbnail_url = select("div.fs-card-img-container img").attr("abs:src")
}
return manga
}
override fun popularMangaNextPageSelector() = "Not needed"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = mutableListOf<SManga>()
document.select(latestUpdatesSelector()).map { mangas.add(latestUpdatesFromElement(it)) }
return MangasPage(mangas.distinctBy { it.url }, false)
}
override fun latestUpdatesSelector() = "table.table-striped tr"
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
with(element) {
manga.url = select("span.fs-comic-title a").attr("href")
manga.title = select("span.fs-comic-title a").text()
manga.thumbnail_url = select("img.fs-chap-img").attr("abs:src")
}
return manga
}
override fun latestUpdatesNextPageSelector() = "not needed"
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
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()
document.select("div.comic-info").let {
it.select("p.fs-comic-label").forEach { el ->
when (el.text().lowercase(Locale.ROOT)) {
"scénario" -> manga.author = el.nextElementSibling()!!.text()
"dessins" -> manga.artist = el.nextElementSibling()!!.text()
"genre" -> manga.genre = el.nextElementSibling()!!.text()
}
}
manga.description = it.select("div.fs-comic-description").text()
manga.thumbnail_url = it.select("img.comic-cover").attr("abs:src")
}
return manga
}
// Chapters
override fun chapterListSelector() = "div.fs-chapter-list div.element"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.url = element.select("div.title a").attr("href")
chapter.name = element.select("div.title a").attr("title")
chapter.date_upload = parseChapterDate(element.select("div.meta_r").text())
return chapter
}
private fun parseChapterDate(date: String): Long {
val lcDate = date.lowercase(Locale.ROOT)
if (lcDate.startsWith("il y a")) {
parseRelativeDate(lcDate).let { return it }
}
// Handle 'day before yesterday', yesterday' and 'today', using midnight
var relativeDate: Calendar? = null
// Result parsed but no year, copy current year over
when {
lcDate.startsWith("avant-hier") -> {
relativeDate = Calendar.getInstance()
relativeDate.add(Calendar.DAY_OF_MONTH, -2) // day before yesterday
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
lcDate.startsWith("hier") -> {
relativeDate = Calendar.getInstance()
relativeDate.add(Calendar.DAY_OF_MONTH, -1) // yesterday
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
lcDate.startsWith("aujourd'hui") -> {
relativeDate = Calendar.getInstance()
relativeDate.set(Calendar.HOUR_OF_DAY, 0) // today
relativeDate.set(Calendar.MINUTE, 0)
relativeDate.set(Calendar.SECOND, 0)
relativeDate.set(Calendar.MILLISECOND, 0)
}
}
return relativeDate?.timeInMillis ?: 0L
}
private fun parseRelativeDate(date: String): Long {
val value = date.split(" ")[3].toIntOrNull()
return if (value != null) {
when (date.split(" ")[4]) {
"minute", "minutes" -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"heure", "heures" -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"jour", "jours" -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"semaine", "semaines" -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"mois" -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
"an", "ans", "année", "années" -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> {
return 0L
}
}
} else {
try {
SimpleDateFormat("dd MMM yyyy", Locale.FRENCH).parse(date.substringAfter("le "))?.time ?: 0
} catch (_: Exception) {
0L
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("div.fs-read img[id]").forEachIndexed { i, img ->
pages.add(Page(i, "", img.attr("abs:src")))
}
return pages
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
override fun getFilterList() = FilterList()
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Bento Manga'
pkgNameSuffix = 'fr.japanread'
extClass = '.BentoManga'
extVersionCode = 15
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,479 @@
package eu.kanade.tachiyomi.extension.fr.japanread
import android.app.Application
import android.net.Uri
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
import java.util.concurrent.TimeUnit
class BentoManga : ParsedHttpSource(), ConfigurableSource {
override val name = "Bento Manga"
override val id: Long = 4697148576707003393
override val baseUrl = "https://www.bentomanga.com"
override val lang = "fr"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(2, 1)
.build()
override fun headersBuilder(): Headers.Builder {
val builder = super.headersBuilder().apply {
set("Referer", "$baseUrl/")
// Headers for homepage + serie page
set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
set("Accept-Language", "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3")
set("Connection", "keep-alive")
set("Sec-Fetch-Dest", "document")
set("Sec-Fetch-Mode", "navigate")
set("Sec-Fetch-Site", "same-origin")
set("Sec-Fetch-User", "?1")
}
val preferences = Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
val userAgent = preferences.getString(USER_AGENT_PREF, "")!!
return if (userAgent.isNotBlank()) {
builder.set("User-Agent", userAgent)
} else {
builder
}
}
// Generic (used by popular/latest/search)
private fun mangaListFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("div").select("div.manga_header h1")
.text()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = element.select("div").select("img[alt=couverture manga]")
.attr("src")
}
}
private fun mangaListSelector() = "div#mangas_content div.manga"
private fun mangaListNextPageSelector() = ".paginator button:contains(>)"
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/manga_list?withoutTypes=5&order_by=views&limit=" + (page - 1), headers)
}
override fun popularMangaSelector() = mangaListSelector()
override fun popularMangaFromElement(element: Element) = mangaListFromElement(element)
override fun popularMangaNextPageSelector() = mangaListNextPageSelector()
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/manga_list?withoutTypes=5&limit=" + (page - 1), headers)
}
override fun latestUpdatesSelector() = mangaListSelector()
override fun latestUpdatesFromElement(element: Element) = mangaListFromElement(element)
override fun latestUpdatesNextPageSelector() = mangaListNextPageSelector()
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// If there is any search text, use text search, otherwise use filter search
val uri = if (query.isNotBlank()) {
Uri.parse("$baseUrl/manga_list?withoutTypes=5")
.buildUpon()
.appendQueryParameter("search", query)
} else {
val uri = Uri.parse("$baseUrl/manga_list?withoutTypes=5").buildUpon()
// Append uri filters
filters.forEach {
if (it is UriFilter) {
it.addToUri(uri)
}
}
uri
}
// Append page number
uri.appendQueryParameter("limit", (page - 1).toString())
return GET(uri.toString())
}
override fun searchMangaSelector() = mangaListSelector()
override fun searchMangaFromElement(element: Element) = mangaListFromElement(element)
override fun searchMangaNextPageSelector() = mangaListNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
title = document.select("div.manga div.manga-infos div.component-manga-title div.component-manga-title_main h1 ")
.text()
artist = document.select("div.datas div.datas_more-artists div.datas_more-artists-people a").text()
author = document.select("div.datas div.datas_more-authors div.datas_more-authors-peoples div a").text()
description = document.select("div.datas div.datas_synopsis").text()
genre = document.select("div.manga div.manga-infos div.component-manga-categories a")
.joinToString(" , ") { it.text() }
status = document.select("div.datas div.datas_more div.datas_more-status div.datas_more-status-data")?.first()?.text()?.let {
when {
it.contains("En cours") -> SManga.ONGOING
it.contains("Terminé") -> SManga.COMPLETED
it.contains("En pause") -> SManga.ON_HIATUS
it.contains("Licencié") -> SManga.LICENSED
it.contains("Abandonné") -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
} ?: SManga.UNKNOWN
thumbnail_url = document.select("img[alt=couverture manga]").attr("src")
}
}
private fun apiHeaders(refererURL: String) = headers.newBuilder().apply {
set("Referer", refererURL)
set("x-requested-with", "XMLHttpRequest")
// without this we get 404 but I don't know why, I cannot find any information about this 'a' header.
// In chrome the value is constantly changing on each request, but giving this fixed value seems to work
set("a", "1df19bce590b")
}.build()
// Chapters
// Subtract relative date
private fun parseRelativeDate(date: String): Long {
val trimmedDate = date.substringAfter("Il y a").trim().split(" ")
val calendar = Calendar.getInstance()
when (trimmedDate[1]) {
"ans" -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
"an" -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
"mois" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
"sem." -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
"j" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
"h" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
"min" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
"s" -> calendar.apply { add(Calendar.SECOND, 0) }
}
return calendar.timeInMillis
}
override fun chapterListSelector() = "div.page_content div.chapters_content div.div-item"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
name = element.select("div.component-chapter-title a span.chapter_volume").text()
setUrlWithoutDomain(element.select("div.component-chapter-title a:not([style*='display:none'])").attr("href"))
date_upload = parseRelativeDate(element.select("div.component-chapter-date").text())
scanlator = element.select("div.component-chapter-teams a span").joinToString(" + ") { it.text() }
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val requestUrl = if (manga.url.startsWith("http")) {
"${manga.url}"
} else {
"$baseUrl${manga.url}"
}
return client.newCall(GET(requestUrl, headers))
.asObservableSuccess()
.map { response ->
chapterListParse(response, requestUrl)
}
}
private fun chapterListParse(response: Response, requestUrl: String): List<SChapter> {
val chapters = mutableListOf<SChapter>()
var document = response.asJsoup()
var moreChapters = true
var nextPage = 1
val pagemax = if (!document.select(".paginator button:contains(>>)").isNullOrEmpty()) {
document.select(".paginator button:contains(>>)")?.first()?.attr("data-limit")?.toInt()?.plus(1)
?: 1
} else {
1
}
// chapters are paginated
while (moreChapters && nextPage <= pagemax) {
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
if (nextPage < pagemax) {
document = client.newCall(GET("$requestUrl?limit=$nextPage", headers)).execute().asJsoup()
nextPage++
} else {
moreChapters = false
}
}
return chapters
}
// Alternative way through API in case jSoup doesn't work anymore
// It gives precise timestamp, but we are not using it
// since the API wrongly returns null for the scanlation group
/*private fun getChapterName(jsonElement: JsonElement): String {
var name = ""
if (jsonElement["volume"].asString != "") {
name += "Tome " + jsonElement["volume"].asString + " "
}
if (jsonElement["chapter"].asString != "") {
name += "Ch " + jsonElement["chapter"].asString + " "
}
if (jsonElement["title"].asString != "") {
if (name != "") {
name += " - "
}
name += jsonElement["title"].asString
}
return name
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val mangaId = document.select("div[data-avg]").attr("data-avg")
client.newCall(GET(baseUrl + document.select("#chapters div[data-row=chapter]").first()!!.select("div.col-lg-5 a").attr("href"), headers)).execute()
val apiResponse = client.newCall(GET("$baseUrl/api/?id=$mangaId&type=manga", apiHeaders())).execute()
val jsonData = apiResponse.body.string()
val json = JsonParser().parse(jsonData).asJsonObject
return json["chapter"].obj.entrySet()
.map {
SChapter.create().apply {
name = getChapterName(it.value.obj)
url = "$baseUrl/api/?id=${it.key}&type=chapter"
date_upload = it.value.obj["timestamp"].asLong * 1000
// scanlator = element.select(".chapter-list-group a").joinToString { it.text() }
}
}
.sortedByDescending { it.date_upload }
}
override fun chapterListSelector() = throw UnsupportedOperationException("Not Used")
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not Used")*/
// Pages
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}", headers)
override fun pageListParse(document: Document): List<Page> {
val chapterId = document.select("meta[data-chapter-id]").attr("data-chapter-id")
val apiRequest = GET("$baseUrl/api/?id=$chapterId&type=chapter", apiHeaders(document.location()))
val apiResponse = client.newCall(apiRequest).execute()
val jsonResult = json.parseToJsonElement(apiResponse.body.string()).jsonObject
val baseImagesUrl = jsonResult["baseImagesUrl"]!!.jsonPrimitive.content
return jsonResult["page_array"]!!.jsonArray.mapIndexed { i, jsonEl ->
Page(i, document.location(), "$baseUrl$baseImagesUrl/${jsonEl.jsonPrimitive.content}")
}
}
override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val newHeaders = headers.newBuilder().apply {
set("Referer", page.url)
set("Accept", "image/avif,image/webp,*/*")
set("Accept-Language", "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3")
set("Connection", "keep-alive")
set("Sec-Fetch-Dest", "document")
set("Sec-Fetch-Mode", "navigate")
set("Sec-Fetch-Site", "same-origin")
set("Sec-Fetch-User", "?1")
}.build()
return GET(page.imageUrl!!, newHeaders)
}
// Filters
override fun getFilterList() = FilterList(
SortFilter(),
TypeFilter(),
StatusFilter(),
GenreFilter(),
)
private class SortFilter : UriSelectFilter(
"Tri",
"order_by",
arrayOf(
Pair("views", "Les + vus"),
Pair("top", "Les mieux notés"),
Pair("name", "A - Z"),
Pair("comment", "Les + commentés"),
Pair("update", "Les + récents"),
Pair("create", "Par date de sortie"),
),
firstIsUnspecified = false,
)
private class TypeFilter : UriSelectFilter(
"Type",
"withTypes",
arrayOf(
Pair("0", "Tous"),
Pair("2", "Manga"),
Pair("3", "Manhwa"),
Pair("4", "Manhua"),
Pair("5", "Novel"),
Pair("6", "Doujinshi"),
),
)
private class StatusFilter : UriSelectFilter(
"Statut",
"status",
arrayOf(
Pair("0", "Tous"),
Pair("1", "En cours"),
Pair("2", "Terminé"),
Pair("3", "En pause"),
Pair("4", "Licencié"),
Pair("5", "Abandonné"),
),
)
private class GenreFilter : UriSelectFilter(
"Genre",
"withCategories",
arrayOf(
Pair("0", "Tous"),
Pair("1", "Action"),
Pair("27", "Adulte"),
Pair("20", "Amitié"),
Pair("21", "Amour"),
Pair("7", "Arts martiaux"),
Pair("3", "Aventure"),
Pair("6", "Combat"),
Pair("5", "Comédie"),
Pair("4", "Drame"),
Pair("12", "Ecchi"),
Pair("16", "Fantastique"),
Pair("29", "Gender Bender"),
Pair("8", "Guerre"),
Pair("22", "Harem"),
Pair("23", "Hentai"),
Pair("15", "Historique"),
Pair("19", "Horreur"),
Pair("13", "Josei"),
Pair("30", "Mature"),
Pair("18", "Mecha"),
Pair("32", "One-shot"),
Pair("42", "Parodie"),
Pair("17", "Policier"),
Pair("25", "Science-fiction"),
Pair("31", "Seinen"),
Pair("10", "Shojo"),
Pair("26", "Shojo Ai"),
Pair("2", "Shonen"),
Pair("35", "Shonen Ai"),
Pair("37", "Smut"),
Pair("14", "Sports"),
Pair("38", "Surnaturel"),
Pair("39", "Tragédie"),
Pair("36", "Tranches de vie"),
Pair("34", "Vie scolaire"),
Pair("24", "Yaoi"),
Pair("41", "Yuri"),
),
)
/**
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
* If an entry is selected it is appended as a query parameter onto the end of the URI.
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
*/
// vals: <name, display>
private open class UriSelectFilter(
displayName: String,
val uriParam: String,
val vals: Array<Pair<String, String>>,
val firstIsUnspecified: Boolean = true,
defaultValue: Int = 0,
) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue),
UriFilter {
override fun addToUri(uri: Uri.Builder) {
if (state != 0 || !firstIsUnspecified) {
uri.appendQueryParameter(uriParam, vals[state].first)
}
}
}
/**
* Represents a filter that is able to modify a URI.
*/
private interface UriFilter {
fun addToUri(uri: Uri.Builder)
}
// From Happymh for the custom User-Agent menu
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Maybe add the choice of a random UA ? (Like Mangathemesia)
EditTextPreference(screen.context).apply {
key = USER_AGENT_PREF
title = TITLE_RANDOM_UA
summary = USER_AGENT_PREF
dialogMessage =
"\n\nPermet d'indiquer un User-Agent custom\n" +
"Après l'ajout + restart de l'application, il faudra charger la page en webview et valider le captcha Cloudflare." +
"\n\nValeur par défaut:\n$DEFAULT_UA"
setDefaultValue(DEFAULT_UA)
setOnPreferenceChangeListener { _, newValue ->
try {
Headers.Builder().add("User-Agent", newValue as String)
Toast.makeText(screen.context, RESTART_APP_STRING, Toast.LENGTH_LONG).show()
summary = newValue
true
} catch (e: Throwable) {
Toast.makeText(screen.context, "$ERROR_USER_AGENT_SETUP ${e.message}", Toast.LENGTH_LONG).show()
false
}
}
}.let(screen::addPreference)
}
companion object {
private const val USER_AGENT_PREF = "Empty"
private const val RESTART_APP_STRING = "Restart Tachiyomi to apply new setting."
private const val ERROR_USER_AGENT_SETUP = "Invalid User-Agent :"
private const val TITLE_RANDOM_UA = "Set custom User-Agent"
private const val DEFAULT_UA = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 (KHTML, like Gecko) Brave/107.0.0.0 Mobile Safari/537.36"
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'LireScan'
pkgNameSuffix = 'fr.lirescan'
extClass = '.LireScan'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.extension.fr.lirescan
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 okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class LireScan : ParsedHttpSource() {
override val name = "LireScan"
override val baseUrl = "https://www.lirescan.me"
override val lang = "fr"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// Popular
// There's no proper directory, get list of manga from dropdown menu available from a manga's page
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/090-eko-to-issho-lecture-en-ligne/", headers)
}
override fun popularMangaSelector() = "div.form-group select option"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.text()
url = element.attr("value")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun latestUpdatesSelector() = "ul#releases > h4"
override fun latestUpdatesFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.text()
url = element.nextElementSibling()!!.select("a").first()!!.attr("href")
.removeSuffix("/").dropLastWhile { it.isDigit() }
}
}
override fun latestUpdatesNextPageSelector(): String? = null
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return popularMangaRequest(1)
}
private fun searchMangaParse(response: Response, query: String): MangasPage {
val mangas = response.asJsoup().select(popularMangaSelector()).toList()
.filter { it.text().contains(query, ignoreCase = true) }
.map { searchMangaFromElement(it) }
return MangasPage(mangas, false)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String? = null
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
thumbnail_url = document.select("a#imglink img").attr("abs:src")
}
}
// Chapters
override fun chapterListSelector() = "select#chapitres option"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.text().let { chNum ->
name = "Chapter $chNum"
setUrlWithoutDomain("${element.ownerDocument()!!.location()}$chNum/")
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val lastPage = document.select("ul.pagination li.page-item:contains(Suiv)").first()!!
.previousElementSibling()!!
.text().toInt()
return IntRange(1, lastPage).map { num -> Page(num - 1, document.location() + num) }
}
override fun imageUrlParse(document: Document): String {
return document.select("a#imglink img").attr("abs:src")
}
override fun getFilterList() = FilterList()
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Mangakawaii'
pkgNameSuffix = 'fr.mangakawaii'
extClass = '.MangaKawaii'
extVersionCode = 37
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1,188 @@
package eu.kanade.tachiyomi.extension.fr.mangakawaii
import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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 eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
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.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.random.Random
/**
* Heavily customized MyMangaReaderCMS source
*/
class MangaKawaii : ParsedHttpSource() {
override val name = "Mangakawaii"
override val baseUrl = "https://www.mangakawaii.io"
private val cdnUrl = "https://cdn.mangakawaii.io"
override val lang = "fr"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(2)
.build()
private val userAgentRandomizer1 = "${Random.nextInt(9).absoluteValue}"
private val userAgentRandomizer2 = "${Random.nextInt(10, 99).absoluteValue}"
private val userAgentRandomizer3 = "${Random.nextInt(100, 999).absoluteValue}"
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/8$userAgentRandomizer1.0.4$userAgentRandomizer3.1$userAgentRandomizer2 Safari/537.36",
)
.add(
"Accept-Language",
lang,
)
// Popular
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = "a.hot-manga__item"
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.select("div.hot-manga__item-caption").select("div.hot-manga__item-name").text().trim()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = "$cdnUrl/uploads" + element.select("a").attr("href") + "/cover/cover_250x350.jpg"
}
// Latest
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesSelector() = ".section__list-group li div.section__list-group-left"
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
title = element.select("a").attr("title")
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = "$cdnUrl/uploads" + element.select("a").attr("href") + "/cover/cover_250x350.jpg"
}
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/search").buildUpon()
.appendQueryParameter("query", query)
.appendQueryParameter("search_type", "manga")
.appendQueryParameter("page", page.toString())
return GET(uri.toString(), headers)
}
override fun searchMangaSelector() = "div.section__list-group-heading"
override fun searchMangaNextPageSelector(): String = "ul.pagination a[rel*=next]"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.select("a").text().trim()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = "$cdnUrl/uploads" + element.select("a").attr("href") + "/cover/cover_250x350.jpg"
}
// Manga details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
thumbnail_url = document.select("div.manga-view__header-image").select("img").attr("abs:src")
description = document.select("dd.text-justify.text-break").text()
author = document.select("a[href*=author]").text()
artist = document.select("a[href*=artist]").text()
genre = document.select("a[href*=category]").joinToString { it.text() }
status = when (document.select("span.badge.bg-success.text-uppercase").text()) {
"En Cours" -> SManga.ONGOING
"Terminé" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// add alternative name to manga description
document.select("span[itemprop=name alternativeHeadline]").joinToString { it.ownText() }.let {
if (it.isNotBlank()) {
description = when {
description.isNullOrBlank() -> "Alternative Names: $it"
else -> "$description\n\nAlternative Names: $it"
}
}
}
}
// Chapter list
override fun chapterListSelector() = throw Exception("Not used")
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val visibleChapters = document.select("tr[class*='volume-']")
if (!visibleChapters.isEmpty()) {
// There is chapters, but the complete list isn't always displayed here
// To get the whole list, let's instead go to a manga page to get the list of links
val someChapter = visibleChapters[0].select(".table__chapter > a").attr("href")
val mangaDocument = client.newCall(GET("$baseUrl$someChapter", headers)).execute().asJsoup()
val notVisibleChapters = mangaDocument.select("#dropdownMenuOffset+ul li")
// If not everything is displayed
if (visibleChapters.count() < notVisibleChapters.count()) {
return notVisibleChapters.map {
SChapter.create().apply {
setUrlWithoutDomain(it.select("a").attr("href"))
name = it.select("a").text()
date_upload = today
}
}
} else {
return visibleChapters.map {
SChapter.create().apply {
setUrlWithoutDomain(it.select("td.table__chapter > a").attr("href"))
name = it.select("td.table__chapter > a span").text()
date_upload = parseDate(it.select("td.table__date").text())
}
}
}
}
return mutableListOf()
}
private val today = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
private fun parseDate(date: String): Long {
return SimpleDateFormat("dd.MM.yyyy", Locale.US).parse(date)?.time ?: today
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val chapterSlug = Regex("""var chapter_slug = "([^"]*)";""").find(document.toString())?.groupValues?.get(1)
val mangaSlug = Regex("""var oeuvre_slug = "([^"]*)";""").find(document.toString())?.groupValues?.get(1)
val pages = mutableListOf<Page>()
Regex(""""page_image":"([^"]*)"""").findAll(document.toString()).asIterable().mapIndexed { i, it ->
pages.add(
Page(
i,
cdnUrl + "/uploads/manga/" + mangaSlug + "/chapters_fr/" + chapterSlug + "/" + it.groupValues[1],
cdnUrl + "/uploads/manga/" + mangaSlug + "/chapters_fr/" + chapterSlug + "/" + it.groupValues[1],
),
)
}
return pages
}
override fun imageUrlParse(document: Document): String = throw Exception("Not used")
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder().apply {
add("Referer", baseUrl)
}.build()
return GET(page.imageUrl!!, imgHeaders)
}
}

1
src/fr/scanmanga/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
local.properties

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Scan-Manga'
pkgNameSuffix = 'fr.scanmanga'
extClass = '.ScanManga'
extVersionCode = 7
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,219 @@
package eu.kanade.tachiyomi.extension.fr.scanmanga
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable
import uy.kohesive.injekt.injectLazy
import kotlin.random.Random
class ScanManga : ParsedHttpSource() {
override val name = "Scan-Manga"
override val baseUrl = "https://www.scan-manga.com"
override val lang = "fr"
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; _ga=GA1.2.${shuffle("123456789")}.${System.currentTimeMillis() / 1000}")
.build()
chain.proceed(newReq)
}.build()
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Accept-Language", "fr-FR")
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/TOP-Manga-Webtoon-22.html", headers)
}
override fun popularMangaSelector() = "div.image_manga a[href]"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("img").attr("title")
setUrlWithoutDomain(element.attr("href"))
thumbnail_url = element.select("img").attr("data-original")
}
}
override fun popularMangaNextPageSelector(): String? = null
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun latestUpdatesSelector() = "#content_news .listing"
override fun latestUpdatesFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.select("a.nom_manga").text()
setUrlWithoutDomain(element.select("a.nom_manga").attr("href"))
/*thumbnail_url = element.select(".logo_manga img").let {
if (it.hasAttr("data-original"))
it.attr("data-original") else it.attr("src")
}*/
// Better not use it, width is too large, which results in terrible image
}
}
override fun latestUpdatesNextPageSelector(): String? = null
// Search
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun searchMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
private fun shuffle(s: String?): String {
val result = StringBuffer(s!!)
var n = result.length
while (n > 1) {
val randomPoint: Int = Random.nextInt(n)
val randomChar = result[randomPoint]
result.setCharAt(n - 1, randomChar)
n--
}
return result.toString()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val searchHeaders = headersBuilder()
.add("Referer", "$baseUrl/scanlation/liste_series.html")
.add("x-requested-with", "XMLHttpRequest")
.build()
return GET("$baseUrl/scanlation/scan.data.json", searchHeaders)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query)
}
}
private fun searchMangaParse(response: Response, query: String): MangasPage {
return MangasPage(parseMangaFromJson(response).mangas.filter { it.title.contains(query, ignoreCase = true) }, false)
}
private fun parseMangaFromJson(response: Response): MangasPage {
val jsonRaw = response.body.string()
if (jsonRaw.isEmpty()) {
return MangasPage(emptyList(), hasNextPage = false)
}
val jsonObj = json.parseToJsonElement(jsonRaw).jsonObject
val mangaList = jsonObj.entries.map { entry ->
SManga.create().apply {
title = Parser.unescapeEntities(entry.key, false)
genre = entry.value.jsonArray[2].jsonPrimitive.content.let {
when {
it.contains("0") -> "Shōnen"
it.contains("1") -> "Shōjo"
it.contains("2") -> "Seinen"
it.contains("3") -> "Josei"
else -> null
}
}
status = entry.value.jsonArray[3].jsonPrimitive.content.let {
when {
it.contains("0") -> SManga.ONGOING // En cours
it.contains("1") -> SManga.ONGOING // En pause
it.contains("2") -> SManga.COMPLETED // Terminé
it.contains("3") -> SManga.COMPLETED // One shot
else -> SManga.UNKNOWN
}
}
url = "/" + entry.value.jsonArray[0].jsonPrimitive.content + "/" +
entry.value.jsonArray[1].jsonPrimitive.content + ".html"
}
}
return MangasPage(mangaList, hasNextPage = false)
}
override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select("h2[itemprop=\"name\"]").text()
author = document.select("li[itemprop=\"author\"] a").joinToString { it.text() }
description = document.select("p[itemprop=\"description\"]").text()
thumbnail_url = document.select(".contenu_fiche_technique .image_manga img").attr("src")
}
// Chapters
override fun chapterListSelector() = throw Exception("Not used")
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select("div.texte_volume_manga ul li.chapitre div.chapitre_nom a").map {
SChapter.create().apply {
name = it.text()
setUrlWithoutDomain(it.attr("href"))
scanlator = document.select("li[itemprop=\"translator\"] a").joinToString { it.text() }
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val docString = document.toString()
var lelUrl = Regex("""['"](http.*?scanmanga.eu.*)['"]""").find(docString)?.groupValues?.get(1)
if (lelUrl == null) {
lelUrl = Regex("""['"](http.*?le[il].scan-manga.com.*)['"]""").find(docString)?.groupValues?.get(1)
}
return Regex("""["'](.*?zoneID.*?pageID.*?siteID.*?)["']""").findAll(docString).toList().mapIndexed { i, pageParam ->
Page(i, document.location(), lelUrl + pageParam.groupValues[1])
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder()
.add("Referer", page.url)
.build()
return GET(page.imageUrl!!, imgHeaders)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Scantrad Union'
pkgNameSuffix = 'fr.scantradunion'
extClass = '.ScantradUnion'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -0,0 +1,171 @@
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 okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
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<Page> {
return document.select("#webtoon a img")
.map { 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")
if (imgElemDataSrc.isNullOrBlank()) imgElemSrc else imgElemDataSrc
}
// Since June 2021, webtoon html has both elements sometimes (data-src and src)
// So there are duplicates when fetching pages
// https://github.com/tachiyomiorg/tachiyomi-extensions/issues/7694
// The distinct() prevent this problem when it happens
.distinct()
.mapIndexed { index: Int, imgUrl: String ->
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().lowercase(Locale.FRENCH)) {
"en cours" -> SManga.ONGOING
"terminé" -> SManga.COMPLETED
"licencié" -> SManga.LICENSED
else -> SManga.UNKNOWN
}
}
}