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
+ }
+ }
+}