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, +)