fix(it/animeunity): Update playlist name, remove no longer existent URL

argument, fix file name without proper extension (#2694)
This commit is contained in:
giorgionegro 2024-01-02 22:50:53 +01:00 committed by GitHub
parent 328b0daff1
commit d6d20c08ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 278 additions and 189 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'AnimeUnity'
pkgNameSuffix = 'it.animeunity'
extClass = '.AnimeUnity'
extVersionCode = 6
extVersionCode = 7
libVersion = '13'
}

View File

@ -36,8 +36,9 @@ import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.text.SimpleDateFormat
class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
class AnimeUnity :
AnimeHttpSource(),
ConfigurableAnimeSource {
override val name = "AnimeUnity"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
@ -56,23 +57,25 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$baseUrl/top-anime?popular=true&page=$page", headers = headers)
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("&quot;", "\"")
}
val animeList = parsed.data.map { ani ->
SAnime.create().apply {
title = ani.title_eng
url = "${ani.id}-${ani.slug}"
thumbnail_url = ani.imageurl ?: ""
val parsed =
response.parseAs<AnimeResponse> {
it
.substringAfter("top-anime animes=\"")
.substringBefore("\"></top-anime>")
.replace("&quot;", "\"")
}
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)
}
@ -84,13 +87,14 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
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 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
@ -99,21 +103,36 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
override fun searchAnimeRequest(
page: Int,
query: String,
filters: AnimeFilterList,
): Request = throw Exception("Not used")
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
override fun fetchSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList,
): Observable<AnimesPage> {
val params = AnimeUnityFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
return client
.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { 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()
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()
@ -121,68 +140,83 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
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", "="))
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)
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}")
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 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())
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 Exception("Not used")
private fun searchAnimeParse(response: Response, page: Int): AnimesPage {
return if (response.request.method == "POST") {
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}"
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
@ -195,21 +229,23 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
val animeDetails = json.decodeFromString<AnimeInfo>(
videoPlayer.attr("anime").replace("&quot;", "\""),
)
val animeDetails =
json.decodeFromString<AnimeInfo>(
videoPlayer.attr("anime").replace("&quot;", "\""),
)
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 ?: "-"}")
}
description =
buildString {
append(animeDetails.plot)
append("\n\nTipo: ${animeDetails.type}")
append("\nStagione: ${animeDetails.season} ${animeDetails.date}")
append("\nValutazione: ★${animeDetails.score ?: "-"}")
}
}
}
@ -225,14 +261,22 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
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", "="))
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)
newHeadersBuilder
.add("X-CSRF-TOKEN", crsfToken)
.add("Content-Type", "application/json")
.add("Referer", response.request.url.toString())
.add("Accept", "application/json, text/plain, */*")
@ -242,27 +286,38 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
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 animeId =
response
.request
.url
.toString()
.substringAfter("/anime/")
.substringBefore("-")
val episodes = json.decodeFromString<List<Episode>>(
videoPlayer.attr("episodes").replace("&quot;", "\""),
)
val episodes =
json.decodeFromString<List<Episode>>(
videoPlayer.attr("episodes").replace("&quot;", "\""),
)
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(),
)
}
},
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) {
@ -291,48 +346,66 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
override fun fetchVideoList(episode: SEpisode): Observable<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 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 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 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 script = iframe.selectFirst("script:containsData(masterPlaylistParams)")!!.data()
val playlistUrl = Regex("""masterPlaylistUrl.*?'(.*?)'""").find(script)!!.groupValues[1]
val expires = Regex("""'expires': ?'(\d+)'""").find(script)!!.groupValues[1]
val canCast = Regex("""'canCast': ?'(\d*)'""").find(script)!!.groupValues[1]
val token = Regex("""'token': ?'([\w-]+)'""").find(script)!!.groupValues[1]
// Get subtitles
val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&canCast=$canCast&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()
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("&canCast=$canCast")
append("&n=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))
}
@ -352,38 +425,51 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
return json.decodeFromString(responseBody)
}
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 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}\""
}
private fun String.falseIfEmpty(): String =
if (this.isEmpty()) {
"false"
} else {
"\"${this}\""
}
@SuppressLint("SimpleDateFormat")
private fun parseDate(date: String): Long {
@ -404,12 +490,13 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
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()
return this
.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.substringBefore("p").toIntOrNull() ?: 0 },
),
).reversed()
}
companion object {
@ -425,35 +512,37 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
summary = PREF_DOMAIN_SUMMARY
dialogTitle = PREF_DOMAIN_TITLE
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
setDefaultValue(PREF_DOMAIN_DEFAULT)
EditTextPreference(screen.context)
.apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
summary = PREF_DOMAIN_SUMMARY
dialogTitle = PREF_DOMAIN_TITLE
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
setDefaultValue(PREF_DOMAIN_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val newValueString = newValue as String
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newValueString.trim()).commit()
}
}.also(screen::addPreference)
setOnPreferenceChangeListener { _, newValue ->
val newValueString = newValue as String
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, newValueString.trim()).commit()
}
}.also(screen::addPreference)
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"
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)
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)
}
}