Initial commit
12
src/ru/animelib/build.gradle
Normal 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')
|
||||
}
|
BIN
src/ru/animelib/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ru/animelib/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/ru/animelib/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/ru/animelib/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/ru/animelib/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
7
src/ru/animevost/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Animevost'
|
||||
extClass = '.Animevost'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ru/animevost/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
src/ru/animevost/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/ru/animevost/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
src/ru/animevost/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
src/ru/animevost/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 260 KiB |
|
@ -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"),
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|