Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,12 @@
ext {
extName = 'Animelib'
extClass = '.Animelib'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation project(':lib:playlist-utils')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

@ -0,0 +1,495 @@
package eu.kanade.tachiyomi.animeextension.ru.animelib
import android.app.Application
import android.util.Base64
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URLDecoder
import java.text.SimpleDateFormat
import java.util.Locale
class Animelib : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Animelib"
override val lang = "ru"
override val supportsLatest = true
private val domain = "anilib.me"
override val baseUrl = "https://$domain/ru"
private val apiUrl = "https://api.lib.social/api"
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private val dateFormatter by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
companion object {
private const val PREF_QUALITY_KEY = "pref_quality"
private val PREF_QUALITY_ENTRIES = arrayOf("360", "720", "1080", "2160")
private const val PREF_USE_MAX_QUALITY_KEY = "pref_use_max_quality"
private const val PREF_USE_MAX_QUALITY_DEFAULT = true
private const val PREF_SERVER_KEY = "pref_server"
private val PREF_SERVER_ENTRIES = arrayOf("Основной", "Резервный 1", "Резервный 2")
private const val PREF_DUB_TEAM_KEY = "prev_dub_team"
private const val PREF_IGNORE_SUBS_KEY = "pref_ignore_subs"
private const val PREF_IGNORE_SUBS_DEFAULT = true
private const val PREF_USE_KODIK_KEY = "pref_use_kodik"
private const val PREF_USE_KODIK_DEFAULT = true
private val ATOB_REGEX = Regex("atob\\([^\"]")
}
// =============================== Preference ===============================
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Предпочитаемый сервер плеера Animelib"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRIES
summary = "%s"
setDefaultValue(PREF_SERVER_ENTRIES[0])
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putString(key, newValue as String).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_USE_MAX_QUALITY_KEY
title = "Использовать максимальное доступное качество"
summary = "Для каждой студии озвучки будет выбрано максимальное качество"
setDefaultValue(PREF_USE_MAX_QUALITY_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
val text = if (value) {
"Предпочитаемое качество пропадет после закрытия окна настроек"
} else {
"Откройте настройки заново чтобы выбрать предпочитаемое качество"
}
Toast.makeText(screen.context, text, Toast.LENGTH_LONG).show()
preferences.edit().putBoolean(key, value).commit()
}
}.also(screen::addPreference)
if (!preferences.getBoolean(PREF_USE_MAX_QUALITY_KEY, true)) {
MultiSelectListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Предпочитаемое качество"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
summary = "При отсутствии нужного качества могут возникать ошибки!"
setDefaultValue(PREF_QUALITY_ENTRIES.toSet())
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
SwitchPreferenceCompat(screen.context).apply {
key = PREF_USE_KODIK_KEY
title = "Включить парсинг видео из плеера Kodik"
summary = "Некоторые видео доступны только в нем, но он может работать нестабильно"
setDefaultValue(PREF_USE_KODIK_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = PREF_IGNORE_SUBS_KEY
title = "Игнорировать субтитры"
summary = "Исключает видео с субтитрами"
setDefaultValue(PREF_IGNORE_SUBS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = PREF_DUB_TEAM_KEY
title = "Предпочитаемые студии озвучки"
summary = "Список студий или ключевых слов через запятую (экспериментальная функция)"
setDefaultValue("")
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putString(key, newValue as String).commit()
}
}.also(screen::addPreference)
}
// =============================== Details ===============================
override fun animeDetailsRequest(anime: SAnime): Request {
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("anime")
url.addPathSegment(anime.url)
url.addQueryParameter("fields[]", "genres")
url.addQueryParameter("fields[]", "summary")
url.addQueryParameter("fields[]", "authors")
url.addQueryParameter("fields[]", "publisher")
url.addQueryParameter("fields[]", "otherNames")
url.addQueryParameter("fields[]", "anime_status_id")
return GET(url.build())
}
override fun getAnimeUrl(anime: SAnime) = "$baseUrl/${anime.url}"
override fun animeDetailsParse(response: Response) = response.parseAs<AnimeInfo>().data.toSAnime()
// =============================== Episodes ===============================
override fun episodeListRequest(anime: SAnime): Request {
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("episodes")
url.addQueryParameter("anime_id", anime.url)
return GET(url.build())
}
override fun episodeListParse(response: Response): List<SEpisode> {
val episodeList = response.parseAs<EpisodeList>()
return episodeList.data.map { it.toSEpisode() }.reversed()
}
// =============================== Video List ===============================
override fun videoListParse(response: Response): List<Video> {
val episodeData = response.parseAs<EpisodeVideoData>()
val videoServer = fetchPreferredVideoServer()
val teams = preferences.getString(PREF_DUB_TEAM_KEY, "")?.split(',')
val preferredTeams = episodeData.data.players?.filter { videoInfo ->
teams.isNullOrEmpty() || teams.any { videoInfo.team.name.contains(it.trim(), true) }
} ?: episodeData.data.players
val useMaxQuality = preferences.getBoolean(
PREF_USE_MAX_QUALITY_KEY,
PREF_USE_MAX_QUALITY_DEFAULT,
)
val videoInfoList = preferredTeams?.filter { videoInfo ->
val quality = bestQuality(videoInfo)
val noneBetter = preferredTeams.none {
bestQuality(it) > quality && it.team.name == videoInfo.team.name
}
noneBetter || !useMaxQuality
} ?: preferredTeams
val ignoreSubs = preferences.getBoolean(PREF_IGNORE_SUBS_KEY, PREF_IGNORE_SUBS_DEFAULT)
return videoInfoList?.flatMap { videoInfo ->
if (ignoreSubs && videoInfo.translationInfo.id == 1) {
return@flatMap emptyList()
}
val playerName = videoInfo.player.lowercase()
when (playerName) {
"kodik" -> kodikVideoLinks(videoInfo.src, videoInfo.team.name)
"animelib" -> animelibVideoLinks(videoInfo, videoServer)
else -> emptyList()
}
} ?: emptyList()
}
override fun videoListRequest(episode: SEpisode) = GET(episode.url)
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int): Request {
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("anime")
url.addQueryParameter("page", page.toString())
url.addQueryParameter("site_id[]", "5")
url.addQueryParameter("links[]", "")
url.addQueryParameter("sort_by", "last_episode_at")
return GET(url.build())
}
// =============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage {
val animeList = response.parseAs<AnimeList>()
val hasNext = !animeList.links?.next.isNullOrEmpty()
val animes = animeList.data.map { it.toSAnime() }
return AnimesPage(animes, hasNext)
}
override fun popularAnimeRequest(page: Int): Request {
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("anime")
url.addQueryParameter("page", page.toString())
url.addQueryParameter("site_id[]", "5")
url.addQueryParameter("links[]", "")
return GET(url.build())
}
// =============================== Search ===============================
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val searchParams = AnimelibFilters.getSearchParameters(filters)
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("anime")
url.addQueryParameter("page", page.toString())
url.addQueryParameter("site_id[]", "5")
url.addQueryParameter("links[]", "")
searchParams.genres.include.forEach { url.addQueryParameter("genres[]", it) }
searchParams.genres.exclude.forEach { url.addQueryParameter("genres_exclude[]", it) }
searchParams.format.forEach { url.addQueryParameter("types[]", it) }
searchParams.pegi.forEach { url.addQueryParameter("caution[]", it) }
searchParams.ongoingStatus.forEach { url.addQueryParameter("status[]", it) }
url.addQueryParameter("sort_by", searchParams.sortOrder)
if (searchParams.sortDirection.isNotEmpty()) {
url.addQueryParameter("sort_type", searchParams.sortDirection)
}
url.addQueryParameter("q", query)
return GET(url.build())
}
override fun getFilterList() = AnimelibFilters.FILTER_LIST
// =============================== Utils ===============================
private fun fetchPreferredVideoServer(): String {
val url = apiUrl.toHttpUrl().newBuilder()
url.addPathSegment("constants")
url.addQueryParameter("fields[]", "videoServers")
val videoServerResponse = client.newCall(GET(url.build())).execute()
val videoServers = videoServerResponse.parseAs<VideoServerData>()
val serverPreference = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_ENTRIES[0])
if (serverPreference.isNullOrEmpty()) {
return videoServers.data.videoServers[0].url
}
for (videoServer in videoServers.data.videoServers) {
if (videoServer.label == serverPreference) {
return videoServer.url
}
}
return videoServers.data.videoServers[0].url
}
private fun kodikVideoLinks(playerUrl: String?, teamName: String): List<Video> {
val useKodik = preferences.getBoolean(PREF_USE_KODIK_KEY, PREF_USE_KODIK_DEFAULT)
if (playerUrl.isNullOrEmpty() || !useKodik) {
return emptyList()
}
val kodikPage = "https:$playerUrl"
val headers = Headers.Builder()
headers.add("Referer", baseUrl)
val kodikPageResponse = client.newCall(GET(kodikPage, headers.build())).execute()
// Parse form parameters for video link request
val page = kodikPageResponse.asJsoup()
val urlParams = page.selectFirst("script:containsData($domain)")?.data()
?: return emptyList()
val formData = urlParams.substringAfter("urlParams = '")
.substringBefore("'")
.parseAs<KodikForm>()
if (formData.dSign.isEmpty()) {
return emptyList()
}
val kodikDomain = formData.pd
val formBody = FormBody.Builder()
formBody.add("d", formData.d)
formBody.add("d_sign", URLDecoder.decode(formData.dSign, "utf-8"))
formBody.add("pd", formData.pd)
formBody.add("pd_sign", URLDecoder.decode(formData.pdSign, "utf-8"))
formBody.add("ref", URLDecoder.decode(formData.ref, "utf-8"))
formBody.add("ref_sign", URLDecoder.decode(formData.refSign, "utf-8"))
val urlParts = playerUrl.split('/')
formBody.add("type", urlParts[3])
formBody.add("id", urlParts[4])
formBody.add("hash", urlParts[5])
val videoInfoRequest = POST("https://$kodikDomain/ftor", body = formBody.build())
val videoInfoResponse = client.newCall(videoInfoRequest).execute()
val kodikData = videoInfoResponse.parseAs<KodikData>()
// Load js with encode algorithm and parse it
val scriptUrl = page.selectFirst("script[src*=player_single]")?.attr("abs:src")
?: return emptyList()
val jsScript = client.newCall(GET(scriptUrl)).execute().body.string()
val atob = ATOB_REGEX.find(jsScript) ?: return emptyList()
var encodeScript = ""
val deque = ArrayDeque<Char>()
deque.addFirst('(')
for (i in atob.range.last..jsScript.length) {
val char = jsScript[i]
if (char in arrayOf('(', '{')) {
deque.addFirst(char)
} else if (char in arrayOf(')', '}')) {
if (deque.isNotEmpty()) {
deque.removeFirst()
}
}
if (deque.isNotEmpty()) {
encodeScript += char
} else {
break
}
}
val useMaxQuality = preferences.getBoolean(
PREF_USE_MAX_QUALITY_KEY,
PREF_USE_MAX_QUALITY_DEFAULT,
)
val qualityPreference = preferences.getStringSet(PREF_QUALITY_KEY, emptySet())
val qualityList = if (useMaxQuality) {
listOf("720")
} else if (!qualityPreference.isNullOrEmpty()) {
qualityPreference.toList()
} else {
listOf("360", "480", "720")
}
val videoList = qualityList.flatMap { quality ->
val quickJs = QuickJs.create()
val videoInfo = when (quality) {
"360" -> kodikData.links.ugly[0].src
"480" -> kodikData.links.bad[0].src
"720" -> kodikData.links.good[0].src
else -> return@flatMap emptyList()
}
val base64Url = quickJs.use {
it.evaluate("t='$videoInfo'; $encodeScript")
}.toString()
val hlsUrl = Base64.decode(base64Url, Base64.DEFAULT).toString(Charsets.UTF_8)
playlistUtils.extractFromHls(
"https:$hlsUrl",
videoNameGen = { "$teamName (${quality}p Kodik)" },
)
}
return videoList
}
private fun animelibVideoLinks(videoInfo: VideoInfo, serverUrl: String): List<Video> {
if (videoInfo.video == null) {
return emptyList()
}
val subtitles = videoInfo.subtitles?.map {
val url = it.src
val lang = "${videoInfo.team.name} (${it.format})"
Track(url, lang)
} ?: emptyList()
val useMaxQuality = preferences.getBoolean(
PREF_USE_MAX_QUALITY_KEY,
PREF_USE_MAX_QUALITY_DEFAULT,
)
val maxQuality = bestQuality(videoInfo)
val qualityPreference = preferences.getStringSet(PREF_QUALITY_KEY, emptySet())
val videoList = videoInfo.video.quality.mapNotNull {
if (useMaxQuality && it.quality != maxQuality) {
return@mapNotNull null
} else if (!useMaxQuality && !qualityPreference.isNullOrEmpty()) {
if (!qualityPreference.contains(it.quality.toString())) {
return@mapNotNull null
}
}
val url = "$serverUrl${it.href}"
val quality = "${videoInfo.team.name} (${it.quality}p Animelib)"
Video(url, quality, url, subtitleTracks = subtitles)
}
return videoList
}
private fun bestQuality(videoInfo: VideoInfo): Int {
return when (videoInfo.player.lowercase()) {
"animelib" -> videoInfo.video?.quality?.maxBy { it.quality }?.quality ?: 0
"kodik" -> 720
else -> 0
}
}
// =============================== Converters ===============================
private fun convertStatus(status: Int): Int {
return when (status) {
1 -> SAnime.ONGOING
2 -> SAnime.COMPLETED
4 -> SAnime.ON_HIATUS
5 -> SAnime.CANCELLED
else -> {
SAnime.UNKNOWN
}
}
}
private fun AnimeData.toSAnime() = SAnime.create().apply {
url = href
title = rusName
thumbnail_url = cover.thumbnail
description = summary
status = convertStatus(animeStatus.id)
author = publisher?.joinToString { it.name }
artist = authors?.joinToString { it.name }
}
private fun EpisodeInfo.toSEpisode() = SEpisode.create().apply {
url = "$apiUrl/episodes/$id"
name = "Сезон $season Серия $number $episodeName"
episode_number = number.toFloat()
date_upload = dateFormatter.parse(date)?.time ?: 0L
}
}

View file

@ -0,0 +1,179 @@
package eu.kanade.tachiyomi.animeextension.ru.animelib
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AnimeStatus(
val id: Int,
)
@Serializable
data class CoverInfo(
val thumbnail: String,
)
@Serializable
data class GenreInfo(
val id: Int,
val name: String,
)
@Serializable
data class PublisherInfo(
val id: Int,
val name: String,
)
@Serializable
data class AuthorInfo(
val id: Int,
val name: String,
)
@Serializable
data class AnimeData(
val id: Int,
@SerialName("rus_name") val rusName: String,
@SerialName("slug_url") val href: String,
@SerialName("status") val animeStatus: AnimeStatus,
val cover: CoverInfo,
// Optional
@SerialName("is_licensed") val licensed: Boolean? = null,
val summary: String? = null,
val genres: List<GenreInfo>? = null,
val publisher: List<PublisherInfo>? = null,
val authors: List<AuthorInfo>? = null,
)
@Serializable
data class PageMetaData(
val next: String? = null,
)
@Serializable
data class AnimeList(
val data: List<AnimeData>,
val links: PageMetaData? = null,
)
@Serializable
data class AnimeInfo(
val data: AnimeData,
)
// ============================== Episode ==============================
@Serializable
data class TeamInfo(
val id: Int,
val name: String,
)
@Serializable
data class VideoQuality(
val href: String,
val quality: Int,
)
@Serializable
data class VideoMetaData(
val id: Int,
val quality: List<VideoQuality>,
)
@Serializable
data class TranslationInfo(
val id: Int,
)
@Serializable
data class SubtitleInfo(
val id: Int,
val format: String,
val src: String,
)
@Serializable
data class VideoInfo(
val id: Int,
val player: String,
val team: TeamInfo,
@SerialName("translation_type") val translationInfo: TranslationInfo,
// Kodik player
val src: String? = null,
// Animelib player
val video: VideoMetaData? = null,
val subtitles: List<SubtitleInfo>? = null,
)
@Serializable
data class EpisodeInfo(
val id: Int,
@SerialName("name") val episodeName: String,
val number: String,
val season: String,
@SerialName("created_at") val date: String,
// Optional
val players: List<VideoInfo>? = null,
)
@Serializable
data class EpisodeVideoData(
val data: EpisodeInfo,
)
@Serializable
data class EpisodeList(
val data: List<EpisodeInfo>,
)
// ============================== VideoServer ==============================
@Serializable
data class VideoServerInfo(
val id: String,
val label: String,
val url: String,
)
@Serializable
data class VideoServers(
val videoServers: List<VideoServerInfo>,
)
@Serializable
data class VideoServerData(
val data: VideoServers,
)
// ============================== Kodik ==============================
@Serializable
data class KodikForm(
val d: String = "",
@SerialName("d_sign") val dSign: String = "",
val pd: String = "",
@SerialName("pd_sign") val pdSign: String = "",
val ref: String = "",
@SerialName("ref_sign") val refSign: String = "",
)
@Serializable
data class KodikVideoInfo(
val src: String,
)
@Serializable
data class KodikVideoQuality(
@SerialName("480") val bad: List<KodikVideoInfo>,
@SerialName("720") val good: List<KodikVideoInfo>,
@SerialName("360") val ugly: List<KodikVideoInfo>,
)
@Serializable
data class KodikData(
val links: KodikVideoQuality,
)

View file

@ -0,0 +1,192 @@
package eu.kanade.tachiyomi.animeextension.ru.animelib
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AnimelibFilters {
open class TriStateFilterList(name: String, values: List<AnimeFilter.TriState>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
class TriFilterVal(name: String) : AnimeFilter.TriState(name)
class GenresFilter : TriStateFilterList("Жанр", AnimelibFiltersData.GENRES.map { TriFilterVal(it.first) })
private inline fun <reified R> AnimeFilterList.getFirst(): R = first { it is R } as R
private inline fun <reified R> AnimeFilterList.parseTriFilter(options: Array<Pair<String, String>>): IncludeExcludeParams {
return (getFirst<R>() as TriStateFilterList).state
.filterNot { it.isIgnored() }
.map { filter -> filter.state to options.find { it.first == filter.name }!!.second }
.groupBy { it.first }
.let { dict ->
val included = dict[AnimeFilter.TriState.STATE_INCLUDE]?.map { it.second }.orEmpty()
val excluded = dict[AnimeFilter.TriState.STATE_EXCLUDE]?.map { it.second }.orEmpty()
IncludeExcludeParams(included, excluded)
}
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
class FormatFilter : CheckBoxFilterList("Формат", AnimelibFiltersData.FORMATS.map { CheckBoxVal(it.first) })
class PegiFilter : CheckBoxFilterList("Возрастной рейтинг", AnimelibFiltersData.PEGI.map { CheckBoxVal(it.first) })
class OngoingFilter : CheckBoxFilterList("Статус тайтла", AnimelibFiltersData.ONGOING_STATUS.map { CheckBoxVal(it.first) })
private inline fun <reified R> AnimeFilterList.parseCheckbox(options: Array<Pair<String, String>>): List<String> {
return (getFirst<R>() as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.toList()
}
class SortFilter : AnimeFilter.Sort(
"Сортировать по",
AnimelibFiltersData.ORDERS.map { it.first }.toTypedArray(),
Selection(0, false),
)
val FILTER_LIST get() = AnimeFilterList(
GenresFilter(),
PegiFilter(),
FormatFilter(),
OngoingFilter(),
SortFilter(),
)
data class IncludeExcludeParams(
val include: List<String> = emptyList(),
var exclude: List<String> = emptyList(),
)
data class FilterSearchParams(
val genres: IncludeExcludeParams = IncludeExcludeParams(),
val format: List<String> = emptyList(),
val pegi: List<String> = emptyList(),
val ongoingStatus: List<String> = emptyList(),
val sortOrder: String = AnimelibFiltersData.ORDERS[0].second,
val sortDirection: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
val sortDirection = filters.getFirst<SortFilter>().state?.let {
if (it.ascending) "asc" else ""
} ?: ""
val sortOrder = filters.getFirst<SortFilter>().state?.let {
AnimelibFiltersData.ORDERS[it.index].second
} ?: ""
return FilterSearchParams(
filters.parseTriFilter<GenresFilter>(AnimelibFiltersData.GENRES),
filters.parseCheckbox<FormatFilter>(AnimelibFiltersData.FORMATS),
filters.parseCheckbox<PegiFilter>(AnimelibFiltersData.PEGI),
filters.parseCheckbox<OngoingFilter>(AnimelibFiltersData.ONGOING_STATUS),
sortOrder,
sortDirection,
)
}
private object AnimelibFiltersData {
val FORMATS = arrayOf(
Pair("TV сериалы", "16"),
Pair("Фильмы", "17"),
Pair("Короткометражное", "18"),
Pair("Спешл", "19"),
Pair("OVA", "20"),
Pair("ONA", "21"),
Pair("Клип", "22"),
)
val GENRES = arrayOf(
Pair("Арт", "32"),
Pair("Безумие", "91"),
Pair("Боевик", "34"),
Pair("Боевые искусства", "35"),
Pair("Вампиры", "36"),
Pair("Военное", "89"),
Pair("Гарем", "37"),
Pair("Гендерная интрига", "38"),
Pair("Героическое фэнтези", "39"),
Pair("Демоны", "81"),
Pair("Детектив", "40"),
Pair("Детское", "88"),
Pair("Дзёсэй", "41"),
Pair("Драма", "43"),
Pair("Игра", "44"),
Pair("Исекай", "79"),
Pair("История", "45"),
Pair("Киберпанк", "46"),
Pair("Кодомо", "76"),
Pair("Комедия", "47"),
Pair("Космос", "83"),
Pair("Магия", "85"),
Pair("Махо-сёдзё", "48"),
Pair("Машины", "90"),
Pair("Меха", "49"),
Pair("Мистика", "50"),
Pair("Музыка", "80"),
Pair("Научная фантастика", "51"),
Pair("Омегаверс", "77"),
Pair("Пародия", "86"),
Pair("Повседневность", "52"),
Pair("Полиция", "82"),
Pair("Постапокалиптика", "53"),
Pair("Приключения", "54"),
Pair("Психология", "55"),
Pair("Романтика", "56"),
Pair("Самурайский боевик", "57"),
Pair("Сверхъестественное", "58"),
Pair("Сёдзё", "59"),
Pair("Сёдзё-ай", "60"),
Pair("Сёнэн", "61"),
Pair("Сёнэн-ай", "62"),
Pair("Спорт", "63"),
Pair("Супер сила", "87"),
Pair("Сэйнэн", "64"),
Pair("Трагедия", "65"),
Pair("Триллер", "66"),
Pair("Ужасы", "67"),
Pair("Фантастика", "68"),
Pair("Фэнтези", "69"),
Pair("Хентай", "84"),
Pair("Школа", "70"),
Pair("Эротика", "71"),
Pair("Этти", "72"),
Pair("Юри", "73"),
Pair("Яой", "74"),
)
val PEGI = arrayOf(
Pair("Нет", "0"),
Pair("6+", "1"),
Pair("12+", "2"),
Pair("16+", "3"),
Pair("18+", "4"),
Pair("18+ (RX)", "5"),
)
val ONGOING_STATUS = arrayOf(
Pair("Онгоинг", "1"),
Pair("Завершён", "2"),
Pair("Анонс", "3"),
Pair("Приостановлен", "4"),
Pair("Выпуск прекращён", "5"),
)
val ORDERS = arrayOf(
Pair("Популярности", "rating_score"),
Pair("Рейтингу", "rate_avg"),
Pair("Просмотрам", "views"),
Pair("Количеству эпизодов", "episodes_count"),
Pair("Дате обновления", "last_episode_at"),
Pair("Дате добавления", "created_at"),
Pair("Названию (A-Z)", "name"),
Pair("Названию (А-Я)", "rus_name"),
)
}
}

View file

@ -0,0 +1,7 @@
ext {
extName = 'Animevost'
extClass = '.Animevost'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View file

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.animeextension.ru.animevost
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
class Animevost : AnimeSourceFactory {
override fun createSources(): List<AnimeSource> = listOf<AnimeSource>(
AnimevostSource("Animevost", "https://animevost.org"),
AnimevostSource("Animevost Mirror", "https://v2.vost.pw"),
)
}

View file

@ -0,0 +1,366 @@
package eu.kanade.tachiyomi.animeextension.ru.animevost
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
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.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class AnimeDescription(
val year: String? = null,
val type: String? = null,
val rating: Int? = null,
val votes: Int? = null,
val description: String? = null,
)
class AnimevostSource(override val name: String, override val baseUrl: String) :
ConfigurableAnimeSource, 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 preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
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, genre: String = "all"): Request {
val headers: Headers =
Headers.headersOf("Content-Type", "application/x-www-form-urlencoded", "charset", "UTF-8")
val url = baseUrl.toHttpUrlOrNull()!!.newBuilder()
var body = FormBody.Builder()
.add("dlenewssortby", sortBy.by)
.add("dledirection", sortDirection.direction)
body = if (genre != "all") {
url.addPathSegment("zhanr")
url.addPathSegment(genre)
body.add("set_new_sort", "dle_sort_cat")
.add("set_direction_sort", "dle_direction_cat")
} else {
body.add("set_new_sort", "dle_sort_main")
.add("set_direction_sort", "dle_direction_main")
}
url.addPathSegment("page")
url.addPathSegment("$page")
return POST(url.toString(), headers, body.build())
}
// Anime details
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
val animeContent = document.select(".shortstory > .shortstoryContent td:first-of-type")
anime.thumbnail_url = "$baseUrl/${animeContent.select("img:first-of-type").attr("src")}"
anime.genre = animeContent.select("p:nth-of-type(2)").text().replace("Жанр: ", "")
anime.author = animeContent.select("p:nth-of-type(5) a").text()
val description = animeContent.select("p:nth-of-type(6) > span").text()
val year = animeContent.select("p:nth-of-type(1)").text().replace("Год выхода: ", "")
val rating = animeContent.select(".current-rating").text().toInt()
val type = animeContent.select("p:nth-of-type(3)").text().replace("Тип: ", "")
val votes = animeContent.select("div:nth-of-type(2) span span").text().toInt()
anime.title = document.select(".shortstory > .shortstoryHead h1").text()
anime.description = formatDescription(
AnimeDescription(
year,
type,
rating,
votes,
description,
),
)
return anime
}
private fun formatDescription(animeData: AnimeDescription): String {
var description = ""
if (animeData.year != null) {
description += "Год: ${animeData.year}\n"
}
if (animeData.rating != null && animeData.votes != null) {
val rating = 5 * animeData.rating / 100
description += "Рейтинг: ${"★".repeat(rating)}${"☆".repeat(Math.max(5 - rating, 0))} (Голосов: ${animeData.votes})\n"
}
if (animeData.type != null) {
description += "Тип: ${animeData.type}\n"
}
if (description.isNotEmpty()) {
description += "\n"
}
description += animeData.description?.replace("<br />", "")
return description
}
// Episode
override fun episodeFromElement(element: Element) = throw UnsupportedOperationException()
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeListParse(response: Response): List<SEpisode> {
val animePage = response.asJsoup()
var episodeScript = animePage.select(".shortstoryContent > script:nth-of-type(2)").html()
episodeScript = episodeScript.substring(episodeScript.indexOf("var data = {") + 12)
val episodes = episodeScript.substring(0, episodeScript.indexOf(",};")).replace("\"", "").split(",")
val episodeList = mutableListOf<SEpisode>()
episodes.forEachIndexed { index, entry ->
episodeList.add(
SEpisode.create().apply {
val id = entry.split(":")[1]
name = entry.split(":")[0]
episode_number = index.toFloat()
url = "/frame5.php?play=$id&old=1"
},
)
}
return episodeList.reversed()
}
// 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 {
return if (query.isNotBlank()) {
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()
POST("$baseUrl/index.php?do=search", headers, body)
} else {
var sortBy = SortBy.DATE
var sortDirection = SortDirection.DESC
var genre = "all"
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
genre = filter.toString()
}
is SortFilter -> {
if (filter.state != null) {
sortBy = sortableList[filter.state!!.index].second
sortDirection = if (filter.state!!.ascending) SortDirection.ASC else SortDirection.DESC
}
}
else -> {}
}
}
animeRequest(page, sortBy, sortDirection, genre)
}
}
override fun searchAnimeSelector() = animeSelector
// Video
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val document = response.asJsoup()
val videoData = document.html().substringAfter("file\":\"").substringBefore("\",").split(",")
videoData.forEach {
val linkData = it.replace("[", "").split("]")
val quality = linkData.first()
val url = linkData.last().split(" or").first()
videoList.add(Video(url, quality, url))
}
return videoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// Filters
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("NOTE: Не работают при текстовом поиске!"),
AnimeFilter.Separator(),
GenreFilter(getGenreList()),
SortFilter(sortableList.map { it.first }.toTypedArray()),
)
private class GenreFilter(genres: Array<Pair<String, String>>) : UriPartFilter("Жанр", genres)
private fun getGenreList() = arrayOf(
Pair("Все", "all"),
Pair("Боевые искусства", "boyevyye-iskusstva"),
Pair("Война", "voyna"),
Pair("Драма", "drama"),
Pair("Детектив", "detektiv"),
Pair("История", "istoriya"),
Pair("Комедия", "komediya"),
Pair("Мистика", "mistika"),
Pair("Меха", "mekha"),
Pair("Махо-сёдзё", "makho-sedze"),
Pair("Музыкальный", "muzykalnyy"),
Pair("Повседневность", "povsednevnost"),
Pair("Приключения", "priklyucheniya"),
Pair("Пародия", "parodiya"),
Pair("Романтика", "romantika"),
Pair("Сёнэн", "senen"),
Pair("Сёдзё", "sedze"),
Pair("Спорт", "sport"),
Pair("Сказка", "skazka"),
Pair("Сёдзё-ай", "sedze-ay"),
Pair("Сёнэн-ай", "senen-ay"),
Pair("Самураи", "samurai"),
Pair("Триллер", "triller"),
Pair("Ужасы", "uzhasy"),
Pair("Фантастика", "fantastika"),
Pair("Фэнтези", "fentezi"),
Pair("Школа", "shkola"),
Pair("Этти", "etti"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
override fun toString() = vals[state].second
}
private val sortableList = listOf(
Pair("Дате", SortBy.DATE),
Pair("Популярности", SortBy.RATING),
Pair("Посещаемости", SortBy.NEWS_READ),
Pair("Комментариям", SortBy.COMM_NUM),
Pair("Алфавиту", SortBy.TITLE),
)
class SortFilter(sortables: Array<String>) : AnimeFilter.Sort("Сортировать по", sortables, Selection(0, false))
// Settings
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("720p", "480p")
entryValues = arrayOf("720", "480")
setDefaultValue("480")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}