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' extName = 'AnimeUnity'
pkgNameSuffix = 'it.animeunity' pkgNameSuffix = 'it.animeunity'
extClass = '.AnimeUnity' extClass = '.AnimeUnity'
extVersionCode = 6 extVersionCode = 7
libVersion = '13' libVersion = '13'
} }

View File

@ -36,8 +36,9 @@ import uy.kohesive.injekt.injectLazy
import java.lang.Exception import java.lang.Exception
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() { class AnimeUnity :
AnimeHttpSource(),
ConfigurableAnimeSource {
override val name = "AnimeUnity" override val name = "AnimeUnity"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! } override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
@ -56,17 +57,19 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/top-anime?popular=true&page=$page", headers = headers)
GET("$baseUrl/top-anime?popular=true&page=$page", headers = headers)
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<AnimeResponse> { val parsed =
it.substringAfter("top-anime animes=\"") response.parseAs<AnimeResponse> {
it
.substringAfter("top-anime animes=\"")
.substringBefore("\"></top-anime>") .substringBefore("\"></top-anime>")
.replace("&quot;", "\"") .replace("&quot;", "\"")
} }
val animeList = parsed.data.map { ani -> val animeList =
parsed.data.map { ani ->
SAnime.create().apply { SAnime.create().apply {
title = ani.title_eng title = ani.title_eng
url = "${ani.id}-${ani.slug}" url = "${ani.id}-${ani.slug}"
@ -84,7 +87,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
override fun latestUpdatesParse(response: Response): AnimesPage { override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val animeList = document.select("div.home-wrapper-body > div.row > div.latest-anime-container").map { val animeList =
document.select("div.home-wrapper-body > div.row > div.latest-anime-container").map {
SAnime.create().apply { SAnime.create().apply {
title = it.select("a > strong").text() title = it.select("a > strong").text()
url = it.selectFirst("a")!!.attr("href").substringAfter("/anime/") url = it.selectFirst("a")!!.attr("href").substringAfter("/anime/")
@ -99,19 +103,34 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Search =============================== // =============================== 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) val params = AnimeUnityFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params)) return client
.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
searchAnimeParse(response, page) searchAnimeParse(response, page)
} }
} }
private fun searchAnimeRequest(page: Int, query: String, filters: AnimeUnityFilters.FilterSearchParams): Request { private fun searchAnimeRequest(
val archivioResponse = client.newCall( page: Int,
query: String,
filters: AnimeUnityFilters.FilterSearchParams,
): Request {
val archivioResponse =
client
.newCall(
GET("$baseUrl/archivio", headers = headers), GET("$baseUrl/archivio", headers = headers),
).execute() ).execute()
@ -121,25 +140,36 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
var newHeadersBuilder = headers.newBuilder() var newHeadersBuilder = headers.newBuilder()
for (cookie in archivioResponse.headers) { for (cookie in archivioResponse.headers) {
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) { 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")) { if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "=")) 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("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") .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")
if (filters.top.isNotEmpty()) { if (filters.top.isNotEmpty()) {
val topHeaders = newHeadersBuilder.add("X-CSRF-TOKEN", crsfToken) 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("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Referer", "$baseUrl/${filters.top}") .add("Referer", "$baseUrl/${filters.top}")
return GET("$baseUrl/${filters.top}", headers = topHeaders.build()) return GET("$baseUrl/${filters.top}", headers = topHeaders.build())
} }
val searchHeaders = newHeadersBuilder val searchHeaders =
newHeadersBuilder
.add("Accept", "application/json, text/plain, */*") .add("Accept", "application/json, text/plain, */*")
.add("Content-Type", "application/json;charset=utf-8") .add("Content-Type", "application/json;charset=utf-8")
.add("Origin", baseUrl) .add("Origin", baseUrl)
@ -147,7 +177,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
.add("X-Requested-With", "XMLHttpRequest") .add("X-Requested-With", "XMLHttpRequest")
.build() .build()
val body = """ val body =
"""
{ {
"title": ${query.falseIfEmpty()}, "title": ${query.falseIfEmpty()},
"type": ${filters.type.falseIfEmpty()}, "type": ${filters.type.falseIfEmpty()},
@ -166,11 +197,15 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used") override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
private fun searchAnimeParse(response: Response, page: Int): AnimesPage { private fun searchAnimeParse(
return if (response.request.method == "POST") { response: Response,
page: Int,
): AnimesPage =
if (response.request.method == "POST") {
val data = response.parseAs<SearchResponse>() val data = response.parseAs<SearchResponse>()
val animeList = data.records.map { val animeList =
data.records.map {
SAnime.create().apply { SAnime.create().apply {
title = it.title_eng title = it.title_eng
thumbnail_url = it.imageurl thumbnail_url = it.imageurl
@ -182,7 +217,6 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
} else { } else {
popularAnimeParse(response) popularAnimeParse(response)
} }
}
override fun getFilterList(): AnimeFilterList = AnimeUnityFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AnimeUnityFilters.FILTER_LIST
@ -195,7 +229,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
val videoPlayer = document.selectFirst("video-player[episodes_count]")!! val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
val animeDetails = json.decodeFromString<AnimeInfo>( val animeDetails =
json.decodeFromString<AnimeInfo>(
videoPlayer.attr("anime").replace("&quot;", "\""), videoPlayer.attr("anime").replace("&quot;", "\""),
) )
@ -204,7 +239,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
status = parseStatus(animeDetails.status) status = parseStatus(animeDetails.status)
artist = animeDetails.studio ?: "" artist = animeDetails.studio ?: ""
genre = animeDetails.genres.joinToString(", ") { it.name } genre = animeDetails.genres.joinToString(", ") { it.name }
description = buildString { description =
buildString {
append(animeDetails.plot) append(animeDetails.plot)
append("\n\nTipo: ${animeDetails.type}") append("\n\nTipo: ${animeDetails.type}")
append("\nStagione: ${animeDetails.season} ${animeDetails.date}") append("\nStagione: ${animeDetails.season} ${animeDetails.date}")
@ -225,14 +261,22 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
var newHeadersBuilder = headers.newBuilder() var newHeadersBuilder = headers.newBuilder()
for (cookie in response.headers) { for (cookie in response.headers) {
if (cookie.first == "set-cookie" && cookie.second.startsWith("XSRF-TOKEN")) { 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")) { if (cookie.first == "set-cookie" && cookie.second.startsWith("animeunity_session")) {
newHeadersBuilder.add("Cookie", cookie.second.substringBefore(";").replace("%3D", "=")) 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("Content-Type", "application/json")
.add("Referer", response.request.url.toString()) .add("Referer", response.request.url.toString())
.add("Accept", "application/json, text/plain, */*") .add("Accept", "application/json, text/plain, */*")
@ -242,14 +286,22 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
val videoPlayer = document.selectFirst("video-player[episodes_count]")!! val videoPlayer = document.selectFirst("video-player[episodes_count]")!!
val episodeCount = videoPlayer.attr("episodes_count").toInt() 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>>( val episodes =
json.decodeFromString<List<Episode>>(
videoPlayer.attr("episodes").replace("&quot;", "\""), videoPlayer.attr("episodes").replace("&quot;", "\""),
) )
episodeList.addAll( episodeList.addAll(
episodes.filter { episodes
.filter {
it.id != null it.id != null
}.map { }.map {
SEpisode.create().apply { SEpisode.create().apply {
@ -257,7 +309,10 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
date_upload = parseDate(it.created_at) date_upload = parseDate(it.created_at)
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
setUrlWithoutDomain( setUrlWithoutDomain(
response.request.url.newBuilder() response
.request
.url
.newBuilder()
.addPathSegment(it.id.toString()) .addPathSegment(it.id.toString())
.toString(), .toString(),
) )
@ -291,46 +346,64 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
val doc =
val doc = client.newCall( client
.newCall(
GET(baseUrl + episode.url, headers), GET(baseUrl + episode.url, headers),
).execute().asJsoup() ).execute()
val iframeUrl = doc.selectFirst("video-player[embed_url]")?.attr("abs:embed_url") ?: error("Failed to extract iframe") .asJsoup()
val iframeUrl =
val iframeHeaders = headers.newBuilder() 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("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
.add("Host", iframeUrl.toHttpUrl().host) .add("Host", iframeUrl.toHttpUrl().host)
.add("Referer", "$baseUrl/") .add("Referer", "$baseUrl/")
.build() .build()
val iframe = client.newCall( val iframe =
client
.newCall(
GET(iframeUrl, headers = iframeHeaders), GET(iframeUrl, headers = iframeHeaders),
).execute().asJsoup() ).execute()
val script = iframe.selectFirst("script:containsData(masterPlaylistParams)")!!.data() .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 playlistUrl = Regex("""masterPlaylistUrl.*?'(.*?)'""").find(script)!!.groupValues[1]
val expires = Regex("""'expires': ?'(\d+)'""").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] val token = Regex("""'token': ?'([\w-]+)'""").find(script)!!.groupValues[1]
// Get subtitles // Get subtitles
val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&canCast=$canCast&n=1" val masterPlUrl = "$playlistUrl?token=$token&expires=$expires&n=1"
val masterPl = client.newCall(GET(masterPlUrl)).execute().body.string() val masterPl =
val subList = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""").findAll(masterPl).map { 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]) Track(it.groupValues[2], it.groupValues[1])
}.toList() }.toList()
Regex("""'token(\d+p?)': ?'([\w-]+)'""").findAll(script).forEach { match -> Regex("""'token(\d+p?)': ?'([\w-]+)'""").findAll(script).forEach { match ->
val quality = match.groupValues[1] val quality = match.groupValues[1]
val videoUrl = buildString { val videoUrl =
buildString {
append(playlistUrl) append(playlistUrl)
append("?type=video&rendition=") append("?type=video&rendition=")
append(quality) append(quality)
append("&token=") append("&token=")
append(match.groupValues[2]) append(match.groupValues[2])
append("&expires=$expires") append("&expires=$expires")
append("&canCast=$canCast")
append("&n=1") append("&n=1")
} }
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subList)) videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subList))
@ -352,18 +425,29 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
return json.decodeFromString(responseBody) return json.decodeFromString(responseBody)
} }
private fun parseStatus(statusString: String): Int = when (statusString) { private fun parseStatus(statusString: String): Int =
when (statusString) {
"In Corso" -> SAnime.ONGOING "In Corso" -> SAnime.ONGOING
"Terminato" -> SAnime.COMPLETED "Terminato" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN else -> SAnime.UNKNOWN
} }
private fun addFromApi(start: Int, end: Int, animeId: String, headers: Headers, url: HttpUrl): List<SEpisode> { private fun addFromApi(
val response = client.newCall( 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), GET("$baseUrl/info_api/$animeId/1?start_range=$start&end_range=$end", headers = headers),
).execute() ).execute()
val json = response.parseAs<ApiResponse>() val json = response.parseAs<ApiResponse>()
return json.episodes.filter { return json
.episodes
.filter {
it.id != null it.id != null
}.map { }.map {
SEpisode.create().apply { SEpisode.create().apply {
@ -371,7 +455,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
date_upload = parseDate(it.created_at) date_upload = parseDate(it.created_at)
episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F episode_number = it.number.split("-")[0].toFloatOrNull() ?: 0F
setUrlWithoutDomain( setUrlWithoutDomain(
url.newBuilder() url
.newBuilder()
.addPathSegment(it.id.toString()) .addPathSegment(it.id.toString())
.toString(), .toString(),
) )
@ -379,7 +464,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
} }
} }
private fun String.falseIfEmpty(): String = if (this.isEmpty()) { private fun String.falseIfEmpty(): String =
if (this.isEmpty()) {
"false" "false"
} else { } else {
"\"${this}\"" "\"${this}\""
@ -404,7 +490,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith( return this
.sortedWith(
compareBy( compareBy(
{ it.quality.contains(quality) }, { it.quality.contains(quality) },
{ it.quality.substringBefore("p").toIntOrNull() ?: 0 }, { it.quality.substringBefore("p").toIntOrNull() ?: 0 },
@ -425,7 +512,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply { EditTextPreference(screen.context)
.apply {
key = PREF_DOMAIN_KEY key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE title = PREF_DOMAIN_TITLE
summary = PREF_DOMAIN_SUMMARY summary = PREF_DOMAIN_SUMMARY
@ -440,7 +528,8 @@ class AnimeUnity : ConfigurableAnimeSource, AnimeHttpSource() {
} }
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context)
.apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p") entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p")