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,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".it.aniplay.AniPlayUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="aniplay.co"
android:pathPattern="/series/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,238 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.AnimeInfoDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.LatestItemDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularAnimeDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.PopularResponseDto
import eu.kanade.tachiyomi.animeextension.it.aniplay.dto.VideoDto
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.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.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AniPlay"
override val baseUrl = "https://aniplay.co"
override val lang = "it"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
override val versionId = 2 // Source was rewritten in Svelte
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) =
GET("$API_URL/advancedSearch?sort=7&page=$page&origin=,,,,,,", headers)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<PopularResponseDto>()
val animes = parsed.data.map(PopularAnimeDto::toSAnime)
return AnimesPage(animes, parsed.pagination.hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$API_URL/latest-episodes?page=$page&type=All")
override fun latestUpdatesParse(response: Response): AnimesPage {
val items = response.parseAs<List<LatestItemDto>>()
val animes = items.mapNotNull { it.serie.firstOrNull()?.toSAnime() }
return AnimesPage(animes, items.size == 20)
}
// =============================== Search ===============================
override fun getFilterList() = AniPlayFilters.FILTER_LIST
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/series/$id"))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
.apply { setUrlWithoutDomain(response.request.url.toString()) }
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val params = AniPlayFilters.getSearchParameters(filters)
val url = "$API_URL/advancedSearch".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("origin", ",,,,,,")
.addQueryParameter("sort", params.order)
.addIfNotBlank("_q", query)
.addIfNotBlank("genres", params.genres)
.addIfNotBlank("country", params.countries)
.addIfNotBlank("types", params.types)
.addIfNotBlank("studios", params.studios)
.addIfNotBlank("status", params.status)
.addIfNotBlank("subbed", params.languages)
.build()
return GET(url, headers)
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response) = SAnime.create().apply {
val script = response.getPageScript()
val jsonString = script.substringAfter("{serie:").substringBefore(",tags") + "}"
val parsed = jsonString.fixJsonString().parseAs<AnimeInfoDto>()
title = parsed.title
genre = parsed.genres.joinToString { it.name }
artist = parsed.studios.joinToString { it.name }
thumbnail_url = parsed.thumbnailUrl
status = when (parsed.status) {
"Completato" -> SAnime.COMPLETED
"In corso" -> SAnime.ONGOING
"Sospeso" -> SAnime.ON_HIATUS
else -> SAnime.UNKNOWN
}
description = buildString {
parsed.description?.also {
append(it, "\n\n")
}
listOf(
"Titolo Alternativo" to parsed.alternative,
"Origine" to parsed.origin,
"Giorno di lancio" to parsed.release_day,
).forEach { (title, value) ->
if (value != null) append(title, ": ", value, "\n")
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val script = response.getPageScript()
val jsonString = script.substringAfter(",episodes:").substringBefore("]},") + "]"
val parsed = jsonString.fixJsonString().parseAs<List<EpisodeDto>>()
return parsed.map {
SEpisode.create().apply {
episode_number = it.number?.toFloatOrNull() ?: 1F
url = "/watch/${it.id}"
name = it.title ?: "Episodio ${it.number}"
date_upload = it.release_date.toDate()
}
}.reversed()
}
// ============================ Video Links =============================
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
override fun videoListParse(response: Response): List<Video> {
val script = response.getPageScript()
val jsonString = script.substringAfter("{episode:").substringBefore(",views") + "}"
val videoUrl = jsonString.fixJsonString().parseAs<VideoDto>().videoLink
return when {
videoUrl.contains(".m3u8") -> playlistUtils.extractFromHls(videoUrl)
else -> listOf(Video(videoUrl, "Default", videoUrl, headers = headers))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES
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)
}
// ============================= Utilities ==============================
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String) = apply {
if (value.isNotBlank()) {
addQueryParameter(query, value)
}
}
// {key:"value"} -> {"key":"value"}
private fun String.fixJsonString() = replace(WRONG_KEY_REGEX) {
"\"${it.groupValues[1]}\":${it.groupValues[2]}"
}
private fun Response.getPageScript() =
asJsoup().selectFirst("script:containsData(const data = )")!!.data()
private fun String?.toDate(): Long {
if (this == null) return 0L
return runCatching { DATE_FORMATTER.parse(trim())?.time }
.getOrNull() ?: 0L
}
companion object {
const val PREFIX_SEARCH = "id:"
private const val API_URL = "https://api.aniplay.co/api/series"
private val WRONG_KEY_REGEX by lazy { Regex("([a-zA-Z_]+):\\s?([\"|0-9|f|t|n|\\[|\\{])") }
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "540p", "480p", "360p", "244p", "144p")
}
}

View file

@ -0,0 +1,281 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object AniPlayFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
val selected get() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.getSelected(): String {
return (first { it is R } as SelectFilter).selected
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
AnimeFilter.Group<AnimeFilter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
private class CheckBoxVal(name: String) : AnimeFilter.CheckBox(name, false)
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
.joinToString(",")
}
internal class OrderFilter : SelectFilter("Ordina per", ORDER_LIST)
internal class GenreFilter : CheckBoxFilterList("Generi", GENRE_LIST)
internal class CountryFilter : CheckBoxFilterList("Paesi", COUNTRY_LIST)
internal class TypeFilter : CheckBoxFilterList("Tipi", TYPE_LIST)
internal class StudioFilter : CheckBoxFilterList("Studio", STUDIO_LIST)
internal class StatusFilter : CheckBoxFilterList("Stato", STATUS_LIST)
internal class LanguageFilter : CheckBoxFilterList("Lingua", LANGUAGE_LIST)
internal val FILTER_LIST get() = AnimeFilterList(
OrderFilter(),
GenreFilter(),
CountryFilter(),
TypeFilter(),
StudioFilter(),
StatusFilter(),
LanguageFilter(),
)
internal data class FilterSearchParams(
val order: String = "1",
val genres: String = "",
val countries: String = "",
val types: String = "",
val studios: String = "",
val status: String = "",
val languages: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.getSelected<OrderFilter>(),
filters.parseCheckbox<GenreFilter>(GENRE_LIST),
filters.parseCheckbox<CountryFilter>(COUNTRY_LIST),
filters.parseCheckbox<TypeFilter>(TYPE_LIST),
filters.parseCheckbox<StudioFilter>(STUDIO_LIST),
filters.parseCheckbox<StatusFilter>(STATUS_LIST),
filters.parseCheckbox<LanguageFilter>(LANGUAGE_LIST),
)
}
private val GENRE_LIST = arrayOf(
Pair("Arti marziali", "35"),
Pair("Automobilismo", "49"),
Pair("Avventura", "3"),
Pair("Azione", "7"),
Pair("Boys Love", "52"),
Pair("Combattimento", "27"),
Pair("Commedia", "13"),
Pair("Cucina", "38"),
Pair("Demenziale", "32"),
Pair("Demoni", "26"),
Pair("Drammatico", "2"),
Pair("Ecchi", "21"),
Pair("Fantasy", "1"),
Pair("Giallo", "34"),
Pair("Gioco", "31"),
Pair("Guerra", "39"),
Pair("Harem", "30"),
Pair("Horror", "14"),
Pair("Isekai", "43"),
Pair("Josei", "47"),
Pair("Magia", "18"),
Pair("Mecha", "25"),
Pair("Militare", "23"),
Pair("Mistero", "5"),
Pair("Musica", "40"),
Pair("Parodia", "42"),
Pair("Politica", "24"),
Pair("Poliziesco", "29"),
Pair("Psicologico", "6"),
Pair("Reverse-harem", "45"),
Pair("Romantico", "8"),
Pair("Samurai", "36"),
Pair("Sci-Fi", "20"),
Pair("Scolastico", "10"),
Pair("Seinen", "28"),
Pair("Sentimentale", "12"),
Pair("Shoujo", "11"),
Pair("Shoujo Ai", "37"),
Pair("Shounen", "16"),
Pair("Shounen Ai", "51"),
Pair("Slice of Life", "19"),
Pair("Sovrannaturale", "22"),
Pair("Spaziale", "48"),
Pair("Splatter", "15"),
Pair("Sport", "41"),
Pair("Storico", "17"),
Pair("Superpoteri", "9"),
Pair("Thriller", "4"),
Pair("Vampiri", "33"),
Pair("Videogame", "44"),
Pair("Yaoi", "50"),
Pair("Yuri", "46"),
)
private val COUNTRY_LIST = arrayOf(
Pair("Corea del Sud", "KR"),
Pair("Cina", "CN"),
Pair("Hong Kong", "HK"),
Pair("Filippine", "PH"),
Pair("Giappone", "JP"),
Pair("Taiwan", "TW"),
Pair("Thailandia", "TH"),
)
private val TYPE_LIST = arrayOf(
Pair("Serie", "1"),
Pair("Movie", "2"),
Pair("OVA", "3"),
Pair("ONA", "4"),
Pair("Special", "5"),
)
private val STUDIO_LIST = arrayOf(
Pair("2:10 AM Animation", "190"),
Pair("5 Inc.", "309"),
Pair("8bit", "17"),
Pair("A-1 Picture", "11"),
Pair("Acca Effe", "180"),
Pair("A.C.G.T.", "77"),
Pair("Actas", "153"),
Pair("AIC ASTA", "150"),
Pair("AIC Build", "46"),
Pair("AIC Classic", "99"),
Pair("AIC Plus+", "26"),
Pair("AIC Spirits", "128"),
Pair("Ajia-Do", "39"),
Pair("Akatsuki", "289"),
Pair("Albacrow", "229"),
Pair("Anima&Co", "161"),
Pair("Animation Planet", "224"),
Pair("Animax", "103"),
Pair("Anpro", "178"),
Pair("APPP", "220"),
Pair("AQUA ARIS", "245"),
Pair("A-Real", "211"),
Pair("ARECT", "273"),
Pair("Arms", "33"),
Pair("Artland", "81"),
Pair("Arvo Animation", "239"),
Pair("Asahi Production", "160"),
Pair("Ashi Production", "307"),
Pair("ASK Animation Studio", "296"),
Pair("Asread", "76"),
Pair("Atelier Pontdarc", "298"),
Pair("AtelierPontdarc", "271"),
Pair("AXsiZ", "70"),
Pair("Bakken Record", "195"),
Pair("Bandai Namco Pictures", "108"),
Pair("Barnum Studio", "191"),
Pair("B.CMAY PICTURES", "135"),
Pair("Bee Media", "262"),
Pair("Bee Train", "98"),
Pair("Bibury Animation Studios", "139"),
Pair("Big FireBird Animation", "141"),
Pair("blade", "212"),
Pair("Bones", "22"),
Pair("Bouncy", "174"),
Pair("Brain's Base", "18"),
Pair("Bridge", "88"),
Pair("B&T", "193"),
Pair("Buemon", "236"),
Pair("BUG FILMS", "314"),
Pair("Bushiroad", "249"),
Pair("C2C", "126"),
Pair("Chaos Project", "247"),
Pair("Charaction", "250"),
Pair("Children's Playground Entertainment", "184"),
Pair("CLAP", "292"),
Pair("CloverWorks", "51"),
Pair("Colored Pencil Animation", "268"),
Pair("CoMix Wave Films", "83"),
Pair("Connect", "185"),
Pair("Craftar Studios", "146"),
Pair("Creators in Pack", "84"),
Pair("C-Station", "72"),
Pair("CyberConnect2", "217"),
Pair("CygamesPictures", "233"),
Pair("DandeLion Animation Studio", "116"),
Pair("Daume", "102"),
Pair("David Production", "73"),
Pair("De Mas & Partners", "207"),
Pair("Diomedea", "21"),
Pair("DLE", "155"),
Pair("DMM.futureworks", "241"),
Pair("DMM pictures", "248"),
Pair("Doga Kobo", "50"),
Pair("domerica", "302"),
Pair("Drive", "226"),
Pair("DR Movie", "113"),
Pair("drop", "130"),
Pair("Dynamo Pictures", "231"),
Pair("E&H Production", "333"),
Pair("EKACHI EPILKA", "151"),
Pair("Emon Animation Company", "149"),
Pair("Emon, Blade", "123"),
Pair("EMT²", "90"),
Pair("Encourage Films", "100"),
Pair("ENGI", "158"),
Pair("evg", "322"),
Pair("EXNOA", "274"),
Pair("Ezo'la", "35"),
Pair("Fanworks", "121"),
Pair("feel.", "37"),
Pair("Felix Film", "163"),
Pair("Frederator Studios", "147"),
Pair("Fugaku", "326"),
Pair("Funimation", "106"),
Pair("Gainax", "43"),
Pair("Gainax Kyoto", "225"),
Pair("Gallop", "109"),
Pair("Gambit", "272"),
Pair("G-angle", "222"),
Pair("Garden Culture", "324"),
)
private val STATUS_LIST = arrayOf(
Pair("Completato", "1"),
Pair("In corso", "2"),
Pair("Sospeso", "3"),
Pair("Annunciato", "4"),
Pair("Non rilasciato", "5"),
)
private val LANGUAGE_LIST = arrayOf(
Pair("Doppiato", "2"),
Pair("RAW", "3"),
Pair("Sottotitolato", "1"),
)
private val ORDER_LIST = arrayOf(
Pair("Rilevanza", "1"),
Pair("Modificato di recente", "2"),
Pair("Aggiunto di recente", "3"),
Pair("Data di rilascio", "4"),
Pair("Nome", "5"),
Pair("Voto", "6"),
Pair("Visualizzazioni", "7"),
Pair("Episodi", "8"),
Pair("Casuale", "9"),
)
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://aniplay.co/series/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AniPlayUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AniPlay.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.animeextension.it.aniplay.dto
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PopularResponseDto(
val data: List<PopularAnimeDto>,
val pagination: PaginationDto,
)
@Serializable
data class PopularAnimeDto(
val id: Int,
@SerialName("title") val name: String,
private val cover: String? = null,
private val main_image: String? = null,
) {
fun toSAnime() = SAnime.create().apply {
url = "/series/$id"
title = name
thumbnail_url = cover ?: main_image
}
}
@Serializable
data class PaginationDto(val page: Int, val pageCount: Int) {
val hasNextPage get() = page < pageCount
}
@Serializable
data class LatestItemDto(val serie: List<PopularAnimeDto>)
@Serializable
data class AnimeInfoDto(
val title: String,
val description: String? = null,
val alternative: String? = null,
val status: String? = null,
val origin: String? = null,
val release_day: String? = null,
val genres: List<NameDto> = emptyList(),
val studios: List<NameDto> = emptyList(),
private val cover: String? = null,
private val main_image: String? = null,
) {
val thumbnailUrl = cover ?: main_image
}
@Serializable
data class NameDto(val name: String)
@Serializable
data class EpisodeDto(
val id: Int,
val title: String? = null,
val number: String? = null,
val release_date: String? = null,
)
@Serializable
data class VideoDto(
private val download_link: String? = null,
private val streaming_link: String? = null,
) {
val videoLink = streaming_link ?: download_link!!
}