diff --git a/src/ru/animevost/AndroidManifest.xml b/src/ru/animevost/AndroidManifest.xml
new file mode 100644
index 000000000..acb4de356
--- /dev/null
+++ b/src/ru/animevost/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/ru/animevost/build.gradle b/src/ru/animevost/build.gradle
new file mode 100644
index 000000000..6a2926a75
--- /dev/null
+++ b/src/ru/animevost/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Animevost'
+ pkgNameSuffix = 'ru.animevost'
+ extClass = '.Animevost'
+ extVersionCode = 1
+ libVersion = '12'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/ru/animevost/res/mipmap-hdpi/ic_launcher.png b/src/ru/animevost/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..4267535f1
Binary files /dev/null and b/src/ru/animevost/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/ru/animevost/res/mipmap-mdpi/ic_launcher.png b/src/ru/animevost/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..f8ddda719
Binary files /dev/null and b/src/ru/animevost/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/ru/animevost/res/mipmap-xhdpi/ic_launcher.png b/src/ru/animevost/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..ac623d5f7
Binary files /dev/null and b/src/ru/animevost/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/ru/animevost/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/animevost/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..11562e9b2
Binary files /dev/null and b/src/ru/animevost/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/ru/animevost/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/animevost/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c32dc7a13
Binary files /dev/null and b/src/ru/animevost/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/Animevost.kt b/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/Animevost.kt
new file mode 100644
index 000000000..42a4c1220
--- /dev/null
+++ b/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/Animevost.kt
@@ -0,0 +1,205 @@
+package eu.kanade.tachiyomi.animeextension.ru.animevost
+
+import eu.kanade.tachiyomi.animeextension.ru.animevost.dto.AnimeDetailsDto
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonObject
+import okhttp3.FormBody
+import okhttp3.Headers
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import java.lang.Exception
+
+class Animevost : ParsedAnimeHttpSource() {
+ private enum class SortBy(val by: String) {
+ RATING("rating"),
+ DATE("date"),
+ NEWS_READ("news_read"),
+ COMM_NUM("comm_num"),
+ TITLE("title"),
+ }
+
+ private enum class SortDirection(val direction: String) {
+ ASC("asc"),
+ DESC("desc"),
+ }
+
+ private val json = Json {
+ isLenient = true
+ ignoreUnknownKeys = true
+ }
+
+ override val name = "Animevost"
+
+ override val baseUrl = "https://animevost.org"
+
+ private val baseApiUrl = "https://api.animevost.org"
+
+ override val lang = "ru"
+
+ override val supportsLatest = true
+
+ private val animeSelector = "div.shortstoryContent"
+
+ private val nextPageSelector = "td.block_4 span:not(.nav_ext) + a"
+
+ private fun animeFromElement(element: Element): SAnime {
+ val anime = SAnime.create()
+ anime.setUrlWithoutDomain(element.select("table div > a").attr("href"))
+ anime.thumbnail_url = baseUrl + element.select("table div > a img").attr("src")
+ anime.title = element.select("table div > a img").attr("alt")
+ return anime
+ }
+
+ private fun animeRequest(page: Int, sortBy: SortBy, sortDirection: SortDirection = SortDirection.DESC): Request {
+ val headers: Headers =
+ Headers.headersOf("Content-Type", "application/x-www-form-urlencoded", "charset", "UTF-8")
+ val body = FormBody.Builder()
+ .add("dlenewssortby", sortBy.by)
+ .add("dledirection", sortDirection.direction)
+ .add("set_new_sort", "dle_sort_main")
+ .add("set_direction_sort", "dle_direction_main")
+ .build()
+
+ return POST("$baseUrl/page/$page", headers, body)
+ }
+
+ private fun parseAnimeIdFromUrl(url: String): String = url.split("/").last().split("-").first()
+
+ // Anime details
+
+ override fun fetchAnimeDetails(anime: SAnime): Observable {
+ val animeId = parseAnimeIdFromUrl(anime.url)
+
+ return client.newCall(GET("$baseApiUrl/animevost/api/v0.2/GetInfo/$animeId"))
+ .asObservableSuccess()
+ .map { response ->
+ animeDetailsParse(response).apply { initialized = true }
+ }
+ }
+
+ override fun animeDetailsParse(response: Response): SAnime {
+ val animeData = json.decodeFromString(AnimeDetailsDto.serializer(), response.body!!.string()).data?.first()
+ val anime = SAnime.create().apply {
+ title = animeData?.title!!
+
+ if (animeData.preview != null) {
+ thumbnail_url = "$baseUrl/" + animeData.preview
+ }
+
+ author = animeData.director
+ description = animeData.description
+
+ if (animeData.timer != null) {
+ status = if (animeData.timer > 0) SAnime.ONGOING else SAnime.COMPLETED
+ }
+
+ genre = animeData.genre
+ }
+
+ return anime
+ }
+
+ override fun animeDetailsParse(document: Document) = throw Exception("not used")
+
+ // Episode
+
+ override fun episodeFromElement(element: Element) = throw Exception("not used")
+
+ override fun episodeListSelector() = throw Exception("not used")
+
+ override fun episodeListRequest(anime: SAnime): Request {
+ val animeId = parseAnimeIdFromUrl(anime.url)
+
+ return GET("$baseApiUrl/animevost/api/v0.2/GetInfo/$animeId")
+ }
+
+ override fun episodeListParse(response: Response): List {
+ val animeData = json.decodeFromString(AnimeDetailsDto.serializer(), response.body!!.string()).data?.first()
+
+ val episodeList = mutableListOf()
+
+ if (animeData?.series != null) {
+ val series = Json.parseToJsonElement(animeData.series.replace("'", "\"")).jsonObject.toMap()
+
+ series.entries.forEachIndexed { index, entry ->
+ episodeList.add(
+ SEpisode.create().apply {
+ val id = entry.value.toString().replace("\"", "")
+ name = entry.key
+ episode_number = index.toFloat()
+ date_upload = System.currentTimeMillis()
+ url = "/frame5.php?play=$id&old=1"
+ }
+ )
+ }
+ }
+
+ return episodeList
+ }
+
+ // Latest
+
+ override fun latestUpdatesFromElement(element: Element) = animeFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = nextPageSelector
+
+ override fun latestUpdatesRequest(page: Int) = animeRequest(page, SortBy.DATE)
+
+ override fun latestUpdatesSelector() = animeSelector
+
+ // Popular Anime
+
+ override fun popularAnimeFromElement(element: Element) = animeFromElement(element)
+
+ override fun popularAnimeNextPageSelector() = nextPageSelector
+
+ override fun popularAnimeRequest(page: Int) = animeRequest(page, SortBy.RATING)
+
+ override fun popularAnimeSelector() = animeSelector
+
+ // Search
+
+ override fun searchAnimeFromElement(element: Element) = animeFromElement(element)
+
+ override fun searchAnimeNextPageSelector() = nextPageSelector
+
+ override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
+ val searchStart = if (page <= 1) 0 else page
+ val resultFrom = (page - 1) * 10 + 1
+ val headers: Headers =
+ Headers.headersOf("Content-Type", "application/x-www-form-urlencoded", "charset", "UTF-8")
+ val body = FormBody.Builder()
+ .add("do", "search")
+ .add("subaction", "search")
+ .add("search_start", searchStart.toString())
+ .add("full_search", "0")
+ .add("result_from", resultFrom.toString())
+ .add("story", query)
+ .build()
+
+ return POST("$baseUrl/index.php?do=search", headers, body)
+ }
+
+ override fun searchAnimeSelector() = animeSelector
+
+ // Video
+
+ override fun videoFromElement(element: Element): Video {
+ return Video(element.attr("href"), element.text(), element.attr("href"), null)
+ }
+
+ override fun videoListSelector() = "a[download]"
+
+ override fun videoUrlParse(document: Document) = throw Exception("not used")
+}
diff --git a/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/dto/AnimevostDto.kt b/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/dto/AnimevostDto.kt
new file mode 100644
index 000000000..d8d2705b3
--- /dev/null
+++ b/src/ru/animevost/src/eu/kanade/tachiyomi/animeextension/ru/animevost/dto/AnimevostDto.kt
@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.animeextension.ru.animevost.dto
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AnimeDetailsDto(
+ @SerialName("data")
+ val data: List? = null,
+)
+
+@Serializable
+data class Data(
+ @SerialName("description")
+ val description: String? = null,
+ @SerialName("director")
+ val director: String? = null,
+ @SerialName("urlImagePreview")
+ val preview: String? = null,
+ @SerialName("year")
+ val year: String? = null,
+ @SerialName("genre")
+ val genre: String? = null,
+ @SerialName("id")
+ val id: Int? = null,
+ @SerialName("timer")
+ val timer: Int? = null,
+ @SerialName("title")
+ val title: String? = null,
+ @SerialName("type")
+ val type: String? = null,
+ @SerialName("series")
+ val series: String? = null,
+)