diff --git a/src/fr/flamescansfr/AndroidManifest.xml b/src/fr/flamescansfr/AndroidManifest.xml new file mode 100644 index 000000000..dbc41f53f --- /dev/null +++ b/src/fr/flamescansfr/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/fr/flamescansfr/build.gradle b/src/fr/flamescansfr/build.gradle index 0a90ad7a2..58002e30f 100644 --- a/src/fr/flamescansfr/build.gradle +++ b/src/fr/flamescansfr/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'Legacy Scans' extClass = '.LegacyScans' - themePkg = 'mangathemesia' - baseUrl = 'https://legacy-scans.com' - overrideVersionCode = 0 + extVersionCode = 31 } apply from: "$rootDir/common.gradle" diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt index 466cdb0c7..9427e1c7a 100644 --- a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt +++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt @@ -1,9 +1,205 @@ package eu.kanade.tachiyomi.extension.fr.flamescansfr -import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia +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.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import rx.Observable +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale -class LegacyScans : MangaThemesia("Legacy Scans", "https://legacy-scans.com", "fr", dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.FRENCH)) { - override val id = 8947802555328550956 +class LegacyScans : HttpSource() { + override val name: String = "Legacy Scans" + + override val lang: String = "fr" + + override val baseUrl: String = "https://legacy-scans.com" + + override val supportsLatest: Boolean = true + + override val versionId: Int = 2 + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(3) + .build() + + private val json: Json by injectLazy() + + override fun popularMangaRequest(page: Int) = GET("$apiUrl/misc/views/all", headers) + + override fun popularMangaParse(response: Response) = + mangasPageParse(response.parseAs>(), false) + + override fun latestUpdatesRequest(page: Int): Request { + val offset = pageOffset(page) + val url = "$apiUrl/misc/comic/home/updates".toHttpUrl().newBuilder() + .addQueryParameter("start", "${offset.first}") + .addQueryParameter("end", "${offset.second}") + .build() + return GET(url, headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage = + mangasPageParse(response.parseAs>()) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(URL_SEARCH_PREFIX)) { + val manga = SManga.create().apply { + url = "/comics/${query.substringAfter(URL_SEARCH_PREFIX)}" + } + client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { response -> + val document = response.asJsoup() + when { + isMangaPage(document) -> { + MangasPage(listOf(mangaDetailsParse(document)), false) + } + else -> MangasPage(emptyList(), false) + } + } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + if (query.isNotBlank()) { + val url = "$apiUrl/misc/home/search".toHttpUrl().newBuilder() + .addQueryParameter("title", query) + .build() + return GET(url, headers) + } + + val defaultSearchOffSet = 18 + + val offset = pageOffset(page, defaultSearchOffSet) + + val url = "$apiUrl/misc/comic/search/query".toHttpUrl().newBuilder() + .addQueryParameter("start", "${offset.first}") + .addQueryParameter("end", "${offset.second}") + + filters.forEach { filter -> + when (filter) { + is SelectFilter -> { + val selected = filter.selectedValue() + if (selected.isBlank()) return@forEach + url.addQueryParameter(filter.field, selected) + } + is GenreList -> { + val genres = filter.state + .filter(GenreCheckBox::state) + .joinToString(",") { it.name } + if (genres.isBlank()) return@forEach + url.addQueryParameter("genreNames", genres) + } + else -> {} + } + } + + return GET(url.build(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val pathSegments = response.request.url.pathSegments + return when { + pathSegments.contains("comic") -> { + mangasPageParse(response.parseAs().comics) + } + else -> { + mangasPageParse(response.parseAs().results, false) + } + } + } + + val mangaDetailsDescriptionSelector = ".serieDescription p" + + override fun mangaDetailsParse(response: Response): SManga = + mangaDetailsParse(response.asJsoup()) + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + return document.select(".chapterList a").map { element -> + SChapter.create().apply { + val spans = element.select("span").map { it.text() } + name = spans.first() + date_upload = spans.last().toDate() + setUrlWithoutDomain(element.absUrl("href")) + } + } + } + + override fun pageListParse(response: Response): List { + val document = response.asJsoup() + return document.select(".readerMainContainer img").mapIndexed { index, image -> + Page(index, document.location(), image.absUrl("src")) + } + } + + override fun imageUrlParse(response: Response): String = "" + + override fun getFilterList(): FilterList { + return FilterList( + SelectFilter("Status", "status", statusList), + SelectFilter("Type", "type", typesList), + GenreList("Genres", genresList), + ) + } + + private fun mangaDetailsParse(document: Document): SManga { + return with(document.selectFirst(".serieContainer")!!) { + SManga.create().apply { + title = selectFirst("h1")!!.text() + thumbnail_url = selectFirst("img")?.absUrl("src") + genre = select(".serieGenre span").joinToString { it.text() } + description = selectFirst(mangaDetailsDescriptionSelector)?.text() + author = selectFirst(".serieAdd p:contains(produit) strong")?.text() + artist = selectFirst(".serieAdd p:contains(Auteur) strong")?.text() + setUrlWithoutDomain(document.location()) + } + } + } + + private fun pageOffset(page: Int, max: Int = 28): Pair { + val start = max * (page - 1) + 1 + val end = max * page + return start to end + } + + private fun mangasPageParse(dto: List, hasNextPage: Boolean = true): MangasPage { + val mangas = dto.map { + SManga.create().apply { + title = it.title + thumbnail_url = it.cover?.let { cover -> "$apiUrl/$cover" } + url = "/comics/${it.slug}" + } + } + return MangasPage(mangas, hasNextPage && mangas.isNotEmpty()) + } + + private inline fun Response.parseAs(): T = + json.decodeFromString(body.string()) + + private fun isMangaPage(document: Document): Boolean = + document.selectFirst(mangaDetailsDescriptionSelector) != null + + private fun String.toDate(): Long = + try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L } + + companion object { + const val apiUrl = "https://api.legacy-scans.com" + const val URL_SEARCH_PREFIX = "slug:" + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH) + } } diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt new file mode 100644 index 000000000..f15592417 --- /dev/null +++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.extension.fr.flamescansfr + +import kotlinx.serialization.Serializable + +@Serializable +class MangaDto( + val cover: String?, + val slug: String, + val title: String, +) + +@Serializable +class SearchDto( + val comics: List, +) + +@Serializable +class SearchQueryDto( + val results: List, +) diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt new file mode 100644 index 000000000..bab29f739 --- /dev/null +++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt @@ -0,0 +1,111 @@ +package eu.kanade.tachiyomi.extension.fr.flamescansfr + +import eu.kanade.tachiyomi.source.model.Filter + +class SelectFilter(name: String, val field: String, values: Array) : + Filter.Select(name, values) { + fun selectedValue() = values[state] +} + +class GenreCheckBox(name: String) : Filter.CheckBox(name) + +class GenreList(title: String, genres: List) : Filter.Group( + title, + genres.map(::GenreCheckBox), +) + +val typesList = arrayOf("", "Manga", "Manhua", "Manhwa", "One shot") + +val statusList = arrayOf("", "En cours", "Terminé", "Annulé", "En pause", "Abandonné") + +val genresList = listOf( + "Action", + "Aventure", + "Arts Martiaux", + "Combat", + "Comédie", + "Drame", + "Fantastique", + "Science-fiction", + "Shonen", + "Amitié", + "Amour", + "Romance", + "Shôjo", + "Tranches de vie", + "Harem", + "Surnaturel", + "Guerre", + "Mystère", + "Fantaisie", + "Psychologique", + "Mature", + "Tragédie", + "Webtoons", + "Seinen", + "Historique", + "Vie scolaire", + "magie", + "One Shot", + "Shôjo Ai", + "Ecchi", + "Horreur", + "Gender Bender", + "Adulte", + "Josei", + "Returner", + "comedy", + "Sports", + "Monstres", + "Realité Virtuel", + "Mecha", + "isekaï", + "Jeux vidéo", + "Inconnu", + "Doujinshi", + "Policier", + "Réincarnation", + "Yuri", + "Sport", + "crime", + "Gangster", + "Police", + "Organisation secrète", + "Magical Girls", + "Bromance", + "Adventure", + "Necromancer", + "Shônen Ai", + "Boxe", + "Parodie", + "Hentai", + "4-koma", + "Voyage Temporel", + "vampires", + "Super héros", + "A", + "c", + "t", + "i", + "o", + "n", + ",", + " ", + "r", + "s", + "M", + "a", + "u", + "x", + "v", + "e", + "C", + "m", + "b", + "S", + "h", + "Smut", + "démons", + "Virtuel world", + "Vengeance", +) diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt new file mode 100644 index 000000000..ab28d83fb --- /dev/null +++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.extension.fr.flamescansfr + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class LegacyScansUrlActivity : Activity() { + + private val tag = javaClass.simpleName + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val item = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${LegacyScans.URL_SEARCH_PREFIX}$item") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e(tag, e.toString()) + } + } else { + Log.e(tag, "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}