Initial commit
2
src/fr/animesama/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/fr/animesama/build.gradle
Normal 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"
|
||||
BIN
src/fr/animesama/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/fr/animesama/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/fr/animesama/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/fr/animesama/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/fr/animesama/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/fr/animesama/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
@@ -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)
|
||||
}
|
||||
}
|
||||
2
src/fr/aralosbd/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/fr/aralosbd/build.gradle
Normal 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"
|
||||
BIN
src/fr/aralosbd/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src/fr/aralosbd/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/fr/aralosbd/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/fr/aralosbd/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/fr/aralosbd/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/fr/aralosbd/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
@@ -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(),
|
||||
)
|
||||
2
src/fr/fmteam/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/fr/fmteam/build.gradle
Normal 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"
|
||||
BIN
src/fr/fmteam/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/fr/fmteam/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/fr/fmteam/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/fr/fmteam/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src/fr/fmteam/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/fr/fmteam/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
2
src/fr/furyosquad/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/fr/furyosquad/build.gradle
Normal 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"
|
||||
BIN
src/fr/furyosquad/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/fr/furyosquad/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/fr/furyosquad/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/fr/furyosquad/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/fr/furyosquad/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/fr/furyosquad/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
@@ -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()
|
||||
}
|
||||
2
src/fr/japanread/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
13
src/fr/japanread/build.gradle
Normal 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"
|
||||
BIN
src/fr/japanread/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 976 B |
BIN
src/fr/japanread/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 716 B |
BIN
src/fr/japanread/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/fr/japanread/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
src/fr/japanread/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 505 B |
BIN
src/fr/japanread/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fr/japanread/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/fr/japanread/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
src/fr/japanread/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/fr/japanread/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/fr/japanread/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/fr/japanread/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
src/fr/japanread/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/fr/japanread/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/fr/japanread/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
2
src/fr/lirescan/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/fr/lirescan/build.gradle
Normal 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"
|
||||
BIN
src/fr/lirescan/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/fr/lirescan/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/fr/lirescan/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/fr/lirescan/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/fr/lirescan/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/fr/lirescan/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
@@ -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()
|
||||
}
|
||||
2
src/fr/mangakawaii/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/fr/mangakawaii/build.gradle
Normal 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"
|
||||
BIN
src/fr/mangakawaii/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/fr/mangakawaii/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/fr/mangakawaii/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/fr/mangakawaii/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/fr/mangakawaii/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/fr/mangakawaii/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -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
@@ -0,0 +1 @@
|
||||
local.properties
|
||||
2
src/fr/scanmanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
13
src/fr/scanmanga/build.gradle
Normal 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"
|
||||
BIN
src/fr/scanmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/fr/scanmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/fr/scanmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/fr/scanmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/fr/scanmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/fr/scanmanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
@@ -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)
|
||||
}
|
||||
}
|
||||
2
src/fr/scantradunion/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/fr/scantradunion/build.gradle
Normal 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"
|
||||
BIN
src/fr/scantradunion/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/fr/scantradunion/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/fr/scantradunion/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/fr/scantradunion/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/fr/scantradunion/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
src/fr/scantradunion/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||