diff --git a/src/fr/aralosbd/AndroidManifest.xml b/src/fr/aralosbd/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/fr/aralosbd/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/fr/aralosbd/build.gradle b/src/fr/aralosbd/build.gradle new file mode 100644 index 000000000..4f9d416b6 --- /dev/null +++ b/src/fr/aralosbd/build.gradle @@ -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 = 4 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/fr/aralosbd/ic_launcher-playstore.png b/src/fr/aralosbd/ic_launcher-playstore.png new file mode 100644 index 000000000..19b77f665 Binary files /dev/null and b/src/fr/aralosbd/ic_launcher-playstore.png differ diff --git a/src/fr/aralosbd/res/mipmap-hdpi/ic_launcher.png b/src/fr/aralosbd/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..283026729 Binary files /dev/null and b/src/fr/aralosbd/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/fr/aralosbd/res/mipmap-mdpi/ic_launcher.png b/src/fr/aralosbd/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..09fa504fa Binary files /dev/null and b/src/fr/aralosbd/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/fr/aralosbd/res/mipmap-xhdpi/ic_launcher.png b/src/fr/aralosbd/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..47b43dad4 Binary files /dev/null and b/src/fr/aralosbd/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/fr/aralosbd/res/mipmap-xxhdpi/ic_launcher.png b/src/fr/aralosbd/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..5a84b5e34 Binary files /dev/null and b/src/fr/aralosbd/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/fr/aralosbd/res/mipmap-xxxhdpi/ic_launcher.png b/src/fr/aralosbd/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f8bcb7a52 Binary files /dev/null and b/src/fr/aralosbd/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/fr/aralosbd/res/web_hi_res_512.png b/src/fr/aralosbd/res/web_hi_res_512.png new file mode 100644 index 000000000..6a45a326f Binary files /dev/null and b/src/fr/aralosbd/res/web_hi_res_512.png differ diff --git a/src/fr/aralosbd/src/eu/kanade/tachiyomi/extension/fr/aralosbd/AralosBD.kt b/src/fr/aralosbd/src/eu/kanade/tachiyomi/extension/fr/aralosbd/AralosBD.kt new file mode 100644 index 000000000..e617f1da1 --- /dev/null +++ b/src/fr/aralosbd/src/eu/kanade/tachiyomi/extension/fr/aralosbd/AralosBD.kt @@ -0,0 +1,230 @@ +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 + +@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 mangas: List = 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? = emptyList(), + val authors: List? = emptyList(), + val translators: List? = emptyList(), + val tags: List? = 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 = emptyList() +) + +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() + } + + 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 { + // This is the search query used for the 'recommandations' in the front page + return GET("$baseUrl/manga/search?s=sort:allviews;limit:16;-id:3", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val searchResult = json.decodeFromString(response.body!!.string()) + + return MangasPage(searchResult.mangas.map(::searchMangaToSManga), false) + } + + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not used.") + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used.") + + 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=text:$query", headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val searchResult = json.decodeFromString(response.body!!.string()) + + return MangasPage(searchResult.mangas.map(::searchMangaToSManga), false) + } + + 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(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 { + val searchResult = json.decodeFromString>(response.body!!.string()) + + val validSearchResults = mutableListOf() + 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 { + val responseBody = client.newCall(GET(response.request.url.toString().replace("chapter?id", "api?get=pages&chapter"), headers)).execute().body + + val pageResult = json.decodeFromString(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 translatorToString(translator: AralosBDTranslator) = translator.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 + } + } +}