Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
7
src/it/animeunity/build.gradle
Normal file
7
src/it/animeunity/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeUnity'
|
||||
extClass = '.AnimeUnity'
|
||||
extVersionCode = 8
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/it/animeunity/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/it/animeunity/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
src/it/animeunity/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/it/animeunity/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
src/it/animeunity/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/it/animeunity/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
src/it/animeunity/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/it/animeunity/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
BIN
src/it/animeunity/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/it/animeunity/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
src/it/animeunity/res/web_hi_res_512.png
Normal file
BIN
src/it/animeunity/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
|
@ -0,0 +1,517 @@
|
|||
package eu.kanade.tachiyomi.animeextension.it.animeunity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.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.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class AnimeUnity :
|
||||
AnimeHttpSource(),
|
||||
ConfigurableAnimeSource {
|
||||
override val name = "AnimeUnity"
|
||||
|
||||
// TODO: Check frequency of url changes to potentially
|
||||
// add back overridable baseurl preference
|
||||
override val baseUrl = "https://www.animeunity.to"
|
||||
|
||||
override val lang = "it"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/top-anime?popular=true&page=$page", headers = headers)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val parsed =
|
||||
response.parseAs<AnimeResponse> {
|
||||
it
|
||||
.substringAfter("top-anime animes=\"")
|
||||
.substringBefore("\"></top-anime>")
|
||||
.replace(""", "\"")
|
||||
}
|
||||
|
||||
val animeList =
|
||||
parsed.data.map { ani ->
|
||||
SAnime.create().apply {
|
||||
title = ani.title_eng
|
||||
url = "${ani.id}-${ani.slug}"
|
||||
thumbnail_url = ani.imageurl ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, parsed.current_page < parsed.last_page)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?anime=$page", headers = headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val animeList =
|
||||
document.select("div.home-wrapper-body > div.row > div.latest-anime-container").map {
|
||||
SAnime.create().apply {
|
||||
title = it.select("a > strong").text()
|
||||
url = it.selectFirst("a")!!.attr("href").substringAfter("/anime/")
|
||||
thumbnail_url = it.select("img").attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
val hasNextPage = document.select("ul.pagination > li.active ~ li").first() != null
|
||||
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: AnimeFilterList,
|
||||
): Request = throw UnsupportedOperationException()
|
||||
|
||||
override suspend fun getSearchAnime(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: AnimeFilterList,
|
||||
): AnimesPage {
|
||||
val params = AnimeUnityFilters.getSearchParameters(filters)
|
||||
return client
|
||||
.newCall(searchAnimeRequest(page, query, params))
|
||||
.awaitSuccess()
|
||||
.use { response ->
|
||||
searchAnimeParse(response, page)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeRequest(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: AnimeUnityFilters.FilterSearchParams,
|
||||
): Request {
|
||||
val archivioResponse =
|
||||
client
|
||||
.newCall(
|
||||
GET("$baseUrl/archivio", headers = headers),
|
||||
).execute()
|
||||
|
||||
val document = archivioResponse.asJsoup()
|
||||
|
||||
val crsfToken = document.select("meta[name=csrf-token]").attr("content")
|
||||
var newHeadersBuilder = headers.newBuilder()
|
||||
for (cookie in archivioResponse.headers) {
|
||||
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) {
|
||||
newHeadersBuilder.add(
|
||||
"X-XSRF-TOKEN",
|
||||
cookie
|
||||
.second
|
||||
.substringAfter("=")
|
||||
.substringBefore(";")
|
||||
.replace("%3D", "="),
|
||||
)
|
||||
}
|
||||
|
||||
if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
|
||||
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "="))
|
||||
}
|
||||
}
|
||||
newHeadersBuilder
|
||||
.add("X-CSRF-TOKEN", crsfToken)
|
||||
.add("Accept-Language", "en-US,en;q=0.5")
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")
|
||||
|
||||
if (filters.top.isNotEmpty()) {
|
||||
val topHeaders =
|
||||
newHeadersBuilder
|
||||
.add("X-CSRF-TOKEN", crsfToken)
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Referer", "$baseUrl/${filters.top}")
|
||||
return GET("$baseUrl/${filters.top}", headers = topHeaders.build())
|
||||
}
|
||||
|
||||
val searchHeaders =
|
||||
newHeadersBuilder
|
||||
.add("Accept", "application/json, text/plain, */*")
|
||||
.add("Content-Type", "application/json;charset=utf-8")
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", archivioResponse.request.url.toString())
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val body =
|
||||
"""
|
||||
{
|
||||
"title": ${query.falseIfEmpty()},
|
||||
"type": ${filters.type.falseIfEmpty()},
|
||||
"year": ${filters.year.falseIfEmpty()},
|
||||
"order": ${filters.order.falseIfEmpty()},
|
||||
"status": ${filters.state.falseIfEmpty()},
|
||||
"genres": ${filters.genre.ifEmpty { "false" }},
|
||||
"offset": ${(page - 1) * 30},
|
||||
"dubbed": ${if (filters.dub.isEmpty()) "false" else "true"},
|
||||
"season": ${filters.season.falseIfEmpty()}
|
||||
}
|
||||
""".trimIndent().toRequestBody("application/json".toMediaType())
|
||||
|
||||
return POST("$baseUrl/archivio/get-animes", body = body, headers = searchHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
|
||||
|
||||
private fun searchAnimeParse(
|
||||
response: Response,
|
||||
page: Int,
|
||||
): AnimesPage =
|
||||
if (response.request.method == "POST") {
|
||||
val data = response.parseAs<SearchResponse>()
|
||||
|
||||
val animeList =
|
||||
data.records.map {
|
||||
SAnime.create().apply {
|
||||
title = it.title_eng
|
||||
thumbnail_url = it.imageurl
|
||||
url = "${it.id}-${it.slug}"
|
||||
}
|
||||
}
|
||||
|
||||
AnimesPage(animeList, data.tot - page * 30 >= 30 && data.tot > 30)
|
||||
} else {
|
||||
popularAnimeParse(response)
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeUnityFilters.FILTER_LIST
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request = GET("$baseUrl/anime/${anime.url}")
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
|
||||
|
||||
val animeDetails =
|
||||
json.decodeFromString<AnimeInfo>(
|
||||
videoPlayer.attr("anime").replace(""", "\""),
|
||||
)
|
||||
|
||||
return SAnime.create().apply {
|
||||
title = animeDetails.title_eng
|
||||
status = parseStatus(animeDetails.status)
|
||||
artist = animeDetails.studio ?: ""
|
||||
genre = animeDetails.genres.joinToString(", ") { it.name }
|
||||
description =
|
||||
buildString {
|
||||
append(animeDetails.plot)
|
||||
append("\n\nTipo: ${animeDetails.type}")
|
||||
append("\nStagione: ${animeDetails.season} ${animeDetails.date}")
|
||||
append("\nValutazione: ★${animeDetails.score ?: "-"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request = GET("$baseUrl/anime/${anime.url}", headers = headers)
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val document = response.asJsoup()
|
||||
|
||||
val crsfToken = document.select("meta[name=csrf-token]").attr("content")
|
||||
var newHeadersBuilder = headers.newBuilder()
|
||||
for (cookie in response.headers) {
|
||||
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) {
|
||||
newHeadersBuilder.add(
|
||||
"X-XSRF-TOKEN",
|
||||
cookie
|
||||
.second
|
||||
.substringAfter("=")
|
||||
.substringBefore(";")
|
||||
.replace("%3D", "="),
|
||||
)
|
||||
}
|
||||
|
||||
if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
|
||||
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "="))
|
||||
}
|
||||
}
|
||||
newHeadersBuilder
|
||||
.add("X-CSRF-TOKEN", crsfToken)
|
||||
.add("Content-Type", "application/json")
|
||||
.add("Referer", response.request.url.toString())
|
||||
.add("Accept", "application/json, text/plain, */*")
|
||||
.add("Accept-Language", "en-US,en;q=0.5")
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
val newHeaders = newHeadersBuilder.build()
|
||||
|
||||
val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
|
||||
val episodeCount = videoPlayer.attr("episodes_count").toInt()
|
||||
val animeId =
|
||||
response
|
||||
.request
|
||||
.url
|
||||
.toString()
|
||||
.substringAfter("/anime/")
|
||||
.substringBefore("-")
|
||||
|
||||
val episodes =
|
||||
json.decodeFromString<List<Episode>>(
|
||||
videoPlayer.attr("episodes").replace(""", "\""),
|
||||
)
|
||||
|
||||
episodeList.addAll(
|
||||
episodes
|
||||
.filter {
|
||||
it.id != null
|
||||
}.map {
|
||||
SEpisode.create().apply {
|
||||
name = "Episode ${it.number}"
|
||||
date_upload = parseDate(it.created_at)
|
||||
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
|
||||
setUrlWithoutDomain(
|
||||
response
|
||||
.request
|
||||
.url
|
||||
.newBuilder()
|
||||
.addPathSegment(it.id.toString())
|
||||
.toString(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (episodeCount > 120) {
|
||||
var start = 121
|
||||
var end = 240
|
||||
|
||||
while (end < episodeCount) {
|
||||
episodeList.addAll(
|
||||
addFromApi(start, end, animeId, newHeaders, response.request.url),
|
||||
)
|
||||
start += 120
|
||||
end += 120
|
||||
}
|
||||
|
||||
if (episodeCount >= start) {
|
||||
episodeList.addAll(
|
||||
addFromApi(start, episodeCount, animeId, newHeaders, response.request.url),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return episodeList.sortedBy { it.episode_number }.reversed()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val doc =
|
||||
client
|
||||
.newCall(
|
||||
GET(baseUrl + episode.url, headers),
|
||||
).execute()
|
||||
.asJsoup()
|
||||
val iframeUrl =
|
||||
doc.selectFirst("video-player[embed_url]")?.attr("abs:embed_url")
|
||||
?: error("Failed to extract iframe")
|
||||
val iframeHeaders =
|
||||
headers
|
||||
.newBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Host", iframeUrl.toHttpUrl().host)
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
|
||||
val iframe =
|
||||
client
|
||||
.newCall(
|
||||
GET(iframeUrl, headers = iframeHeaders),
|
||||
).execute()
|
||||
.asJsoup()
|
||||
val scripts = iframe.select("script")
|
||||
val script = scripts.find { it.data().contains("masterPlaylist") }!!.data().replace("\n", "\t")
|
||||
var playlistUrl = Regex("""url: ?'(.*?)'""").find(script)!!.groupValues[1]
|
||||
val filename = playlistUrl.slice(playlistUrl.lastIndexOf("/") + 1 until playlistUrl.length)
|
||||
if (!filename.endsWith(".m3u8")) {
|
||||
playlistUrl = playlistUrl.replace(filename, filename + ".m3u8")
|
||||
}
|
||||
|
||||
val expires = Regex("""'expires': ?'(\d+)'""").find(script)!!.groupValues[1]
|
||||
val token = Regex("""'token': ?'([\w-]+)'""").find(script)!!.groupValues[1]
|
||||
// Get subtitles
|
||||
val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&n=1"
|
||||
val masterPl =
|
||||
client
|
||||
.newCall(GET(masterPlUrl))
|
||||
.execute()
|
||||
.body
|
||||
.string()
|
||||
val subList =
|
||||
Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||
.findAll(masterPl)
|
||||
.map {
|
||||
Track(it.groupValues[2], it.groupValues[1])
|
||||
}.toList()
|
||||
Regex("""'token(\d+p?)': ?'([\w-]+)'""").findAll(script).forEach { match ->
|
||||
val quality = match.groupValues[1]
|
||||
|
||||
val videoUrl =
|
||||
buildString {
|
||||
append(playlistUrl)
|
||||
append("?type=video&rendition=")
|
||||
append(quality)
|
||||
append("&token=")
|
||||
append(match.groupValues[2])
|
||||
append("&expires=$expires")
|
||||
append("&n=1")
|
||||
}
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subList))
|
||||
}
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun parseStatus(statusString: String): Int =
|
||||
when (statusString) {
|
||||
"In Corso" -> SAnime.ONGOING
|
||||
"Terminato" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
|
||||
private fun addFromApi(
|
||||
start: Int,
|
||||
end: Int,
|
||||
animeId: String,
|
||||
headers: Headers,
|
||||
url: HttpUrl,
|
||||
): List<SEpisode> {
|
||||
val response =
|
||||
client
|
||||
.newCall(
|
||||
GET("$baseUrl/info_api/$animeId/1?start_range=$start&end_range=$end", headers = headers),
|
||||
).execute()
|
||||
val json = response.parseAs<ApiResponse>()
|
||||
return json
|
||||
.episodes
|
||||
.filter {
|
||||
it.id != null
|
||||
}.map {
|
||||
SEpisode.create().apply {
|
||||
name = "Episode ${it.number}"
|
||||
date_upload = parseDate(it.created_at)
|
||||
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
|
||||
setUrlWithoutDomain(
|
||||
url
|
||||
.newBuilder()
|
||||
.addPathSegment(it.id.toString())
|
||||
.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.falseIfEmpty(): String =
|
||||
if (this.isEmpty()) {
|
||||
"false"
|
||||
} else {
|
||||
"\"${this}\""
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun parseDate(date: String): Long {
|
||||
val knownPatterns: MutableList<SimpleDateFormat> = ArrayList()
|
||||
knownPatterns.add(SimpleDateFormat("yyyy-MM-dd hh:mm:ss"))
|
||||
|
||||
for (pattern in knownPatterns) {
|
||||
try {
|
||||
// Take a try
|
||||
return pattern.parse(date)!!.time
|
||||
} catch (e: Throwable) {
|
||||
// Loop on
|
||||
}
|
||||
}
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return this
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.substringBefore("p").toIntOrNull() ?: 0 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context)
|
||||
.apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240", "80")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package eu.kanade.tachiyomi.animeextension.it.animeunity
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AnimeResponse(
|
||||
val current_page: Int,
|
||||
val last_page: Int,
|
||||
val data: List<Anime>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Anime(
|
||||
val id: Int,
|
||||
val slug: String,
|
||||
val title_eng: String,
|
||||
val imageurl: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val number: String,
|
||||
val created_at: String,
|
||||
val id: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiResponse(
|
||||
val episodes: List<Episode>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ServerResponse(
|
||||
val name: String,
|
||||
val client_ip: String,
|
||||
val folder_id: String,
|
||||
val proxy_download: Int,
|
||||
val storage_download: StorageDownload,
|
||||
) {
|
||||
@Serializable
|
||||
data class StorageDownload(
|
||||
val number: Int,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LinkData(
|
||||
val id: String,
|
||||
val file_name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnimeInfo(
|
||||
val title_eng: String,
|
||||
val imageurl: String,
|
||||
val plot: String,
|
||||
val date: String,
|
||||
val season: String,
|
||||
val slug: String,
|
||||
val id: Int,
|
||||
val type: String,
|
||||
val status: String,
|
||||
val genres: List<Genre>,
|
||||
val studio: String? = null,
|
||||
val score: String? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Genre(
|
||||
val name: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
val records: List<AnimeInfo>,
|
||||
val tot: Int,
|
||||
)
|
|
@ -0,0 +1,204 @@
|
|||
package eu.kanade.tachiyomi.animeextension.it.animeunity
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
object AnimeUnityFilters {
|
||||
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return this.filterIsInstance<R>().joinToString("") {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
}
|
||||
|
||||
class TopFilter : QueryPartFilter("Top Anime", AnimeUnityFiltersData.TOP)
|
||||
|
||||
class GenreFilter : CheckBoxFilterList(
|
||||
"Genere",
|
||||
AnimeUnityFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class YearFilter : QueryPartFilter("Anno", AnimeUnityFiltersData.YEAR)
|
||||
|
||||
class OrderFilter : QueryPartFilter("Ordina", AnimeUnityFiltersData.ORDER)
|
||||
|
||||
class StateFilter : QueryPartFilter("Stato", AnimeUnityFiltersData.STATE)
|
||||
|
||||
class TypeFilter : QueryPartFilter("Tipo", AnimeUnityFiltersData.TYPE)
|
||||
|
||||
class SeasonFilter : QueryPartFilter("Stagione", AnimeUnityFiltersData.SEASON)
|
||||
|
||||
class DubFilter : QueryPartFilter("Dub ITA", AnimeUnityFiltersData.DUB)
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
AnimeFilter.Header("Le migliori pagine di anime"),
|
||||
AnimeFilter.Header("Nota: ignora altri filtri"),
|
||||
TopFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
GenreFilter(),
|
||||
YearFilter(),
|
||||
OrderFilter(),
|
||||
StateFilter(),
|
||||
TypeFilter(),
|
||||
SeasonFilter(),
|
||||
DubFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val top: String = "",
|
||||
val genre: String = "",
|
||||
val year: String = "",
|
||||
val order: String = "",
|
||||
val state: String = "",
|
||||
val type: String = "",
|
||||
val season: String = "",
|
||||
val dub: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
val genre: String = filters.filterIsInstance<GenreFilter>()
|
||||
.first()
|
||||
.state.filter { it.state }.joinToString(",") { format ->
|
||||
buildJsonObject {
|
||||
put("id", AnimeUnityFiltersData.GENRE.find { it.first == format.name }!!.second.toInt())
|
||||
put("name", AnimeUnityFiltersData.GENRE.find { it.first == format.name }!!.first)
|
||||
}.toString()
|
||||
}
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<TopFilter>(),
|
||||
if (genre.isEmpty()) "" else "[$genre]",
|
||||
filters.asQueryPart<YearFilter>(),
|
||||
filters.asQueryPart<OrderFilter>(),
|
||||
filters.asQueryPart<StateFilter>(),
|
||||
filters.asQueryPart<TypeFilter>(),
|
||||
filters.asQueryPart<SeasonFilter>(),
|
||||
filters.asQueryPart<DubFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private object AnimeUnityFiltersData {
|
||||
val ANY = Pair("Any", "")
|
||||
|
||||
val TOP = arrayOf(
|
||||
Pair("Nessuno", ""),
|
||||
Pair("Tutto", "top-anime"),
|
||||
Pair("In corso", "top-anime?status=In Corso"),
|
||||
Pair("In arrivo", "top-anime?status=In uscita prossimamente"),
|
||||
Pair("TV", "top-anime?type=TV"),
|
||||
Pair("Movie", "top-anime?type=Movie"),
|
||||
Pair("OVA", "top-anime?type=OVA"),
|
||||
Pair("ONA", "top-anime?type=ONA"),
|
||||
Pair("Special", "top-anime?type=Special"),
|
||||
Pair("Popolari", "top-anime?popular=true"),
|
||||
Pair("Preferiti", "top-anime?order=favorites"),
|
||||
Pair("Più visti", "top-anime?order=most_viewed"),
|
||||
)
|
||||
|
||||
val GENRE = arrayOf(
|
||||
Pair("Action", "51"),
|
||||
Pair("Adventure", "21"),
|
||||
Pair("Cars", "29"),
|
||||
Pair("Comedy", "37"),
|
||||
Pair("Dementia", "43"),
|
||||
Pair("Demons", "13"),
|
||||
Pair("Drama", "22"),
|
||||
Pair("Ecchi", "5"),
|
||||
Pair("Fantasy", "9"),
|
||||
Pair("Game", "44"),
|
||||
Pair("Harem", "15"),
|
||||
Pair("Hentai", "4"),
|
||||
Pair("Historical", "30"),
|
||||
Pair("Horror", "3"),
|
||||
Pair("Josei", "45"),
|
||||
Pair("Kids", "14"),
|
||||
Pair("Magic", "23"),
|
||||
Pair("Martial Arts", "Martial 31"),
|
||||
Pair("Mecha", "38"),
|
||||
Pair("Military", "46"),
|
||||
Pair("Music", "16"),
|
||||
Pair("Mystery", "24"),
|
||||
Pair("Parody", "32"),
|
||||
Pair("Police", "39"),
|
||||
Pair("Psychological", "47"),
|
||||
Pair("Romance", "17"),
|
||||
Pair("Samurai", "25"),
|
||||
Pair("School", "33"),
|
||||
Pair("Sci-fi", "Sci-40"),
|
||||
Pair("Seinen", "49"),
|
||||
Pair("Shoujo", "18"),
|
||||
Pair("Shoujo Ai", "Shoujo 26"),
|
||||
Pair("Shounen", "34"),
|
||||
Pair("Shounen Ai", "Shounen 41"),
|
||||
Pair("Slice of Life", "Slice of 50"),
|
||||
Pair("Space", "19"),
|
||||
Pair("Splatter", "52"),
|
||||
Pair("Sports", "27"),
|
||||
Pair("Super Power", "Super 35"),
|
||||
Pair("Supernatural", "42"),
|
||||
Pair("Thriller", "48"),
|
||||
Pair("Vampire", "20"),
|
||||
Pair("Yaoi", "28"),
|
||||
Pair("Yuri", "36"),
|
||||
)
|
||||
|
||||
val ORDER = arrayOf(
|
||||
ANY,
|
||||
Pair("Lista A-Z", "Lista A-Z"),
|
||||
Pair("Lista Z-A", "Lista Z-A"),
|
||||
Pair("Popolarità", "Popolarità"),
|
||||
Pair("Valutazione", "Valutazione"),
|
||||
)
|
||||
|
||||
val STATE = arrayOf(
|
||||
ANY,
|
||||
Pair("In Corso", "In Corso"),
|
||||
Pair("Terminato", "Terminato"),
|
||||
Pair("In Uscita", "In Uscita"),
|
||||
Pair("Droppato", "Droppato"),
|
||||
)
|
||||
|
||||
val TYPE = arrayOf(
|
||||
ANY,
|
||||
Pair("TV", "TV"),
|
||||
Pair("OVA", "OVA"),
|
||||
Pair("ONA", "ONA"),
|
||||
Pair("Special", "Special"),
|
||||
Pair("Movie", "Movie"),
|
||||
)
|
||||
|
||||
val SEASON = arrayOf(
|
||||
ANY,
|
||||
Pair("Inverno", "Inverno"),
|
||||
Pair("Primavera", "Primavera"),
|
||||
Pair("Estate", "Estate"),
|
||||
Pair("Autunno", "Autunno"),
|
||||
)
|
||||
|
||||
val DUB = arrayOf(
|
||||
Pair("No", ""),
|
||||
Pair("Sì", "true"),
|
||||
)
|
||||
|
||||
val YEAR = arrayOf(ANY) + (1969..2024).map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.reversed().toTypedArray()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue