New source: AnimesROLL (#1246)

* feat: Create AnimesROLL base

* feat: Implement latest animes page

* feat: Implement (pseudo)Popular animes page

* feat: Implement episode list and video list

* feat: Implement search page

* refactor: Use the data embedded on some pages instead of obscure APIs

* fix: Remove zeros from anime details page

* refactor: Minor refactor on a variable

* fix: Make URL Intent handler work well
This commit is contained in:
Claudemirovsky
2023-02-06 07:38:02 -03:00
committed by GitHub
parent 0970bce223
commit 1e7055bd68
10 changed files with 325 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.animesroll.AnimesROLLUrlActivity"
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="www.anroll.net"
android:pathPattern="/a/..*"
android:scheme="https" />
<data
android:host="www.anroll.net"
android:pathPattern="/f/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'AnimesROLL'
pkgNameSuffix = 'pt.animesroll'
extClass = '.AnimesROLL'
extVersionCode = 1
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.AnimeDataDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.AnimeInfoDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.EpisodeDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.LatestAnimeDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.PagePropDto
import eu.kanade.tachiyomi.animeextension.pt.animesroll.dto.SearchResultsDto
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.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
class AnimesROLL : AnimeHttpSource() {
override val name = "AnimesROLL"
override val baseUrl = "https://www.anroll.net"
override val lang = "pt-BR"
override val supportsLatest = true
override fun headersBuilder() = Headers.Builder().add("Referer", baseUrl)
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}
// ============================== Popular ===============================
// The site doesn't have a popular anime tab, so we use the home page instead (latest anime).
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeParse(response: Response) = latestUpdatesParse(response)
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
val originalUrl = response.request.url.toString()
return if ("/f/" in originalUrl) {
val od = response.asJsoup().parseAs<AnimeInfoDto>().animeData.od
val episode = SEpisode.create().apply {
url = "$NEW_API_URL/od/$od/filme.mp4"
name = "Filme"
episode_number = 0F
}
listOf(episode)
} else {
val epdata = response.asJsoup().parseAs<EpisodeDto>()
val urlStart = "https://cdn-01.animesroll.com/hls/animes/${epdata.anime.slug}"
(epdata.total_ep downTo 1).map {
SEpisode.create().apply {
episode_number = it.toFloat()
val fixedNum = it.toString().padStart(3, '0')
name = "Episódio #$fixedNum"
url = "$urlStart/$fixedNum.mp4/media-1/stream.m3u8"
}
}
}
}
// ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val epUrl = episode.url
return Observable.just(listOf(Video(epUrl, "default", epUrl)))
}
override fun videoListRequest(episode: SEpisode): Request {
TODO("Not yet implemented")
}
override fun videoListParse(response: Response): List<Video> {
TODO("Not yet implemented")
}
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime {
val doc = response.asJsoup()
val anime = doc.parseAs<AnimeInfoDto>().animeData
return anime.toSAnime().apply {
author = if (anime.director != "0") anime.director else null
var desc = anime.description.ifNotEmpty { it + "\n" }
desc += anime.duration.ifNotEmpty { "\nDuração: $it" }
desc += anime.animeCalendar?.let {
it.ifNotEmpty { "\nLança toda(o) $it" }
} ?: ""
description = desc
genre = doc.select("div#generos > a").joinToString(", ") { it.text() }
status = if (anime.animeCalendar == null) SAnime.COMPLETED else SAnime.ONGOING
}
}
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response, path: String): AnimesPage {
val details = animeDetailsParse(response)
details.url = "/$path"
return AnimesPage(listOf(details), false)
}
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) {
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess()
.map { response ->
searchAnimeByPathParse(response, path)
}
} else {
super.fetchSearchAnime(page, query, filters)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val results = response.parseAs<SearchResultsDto>()
val animes = (results.animes + results.movies).map { it.toSAnime() }
return AnimesPage(animes, false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return GET("$NEW_API_URL/search?q=$query")
}
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.asJsoup().parseAs<LatestAnimeDto>()
val animes = parsed.animes.map { it.toSAnime() }
return AnimesPage(animes, false)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/lancamentos")
// ============================= Utilities ==============================
private inline fun <reified T> Document.parseAs(): T {
val nextData = this.selectFirst("script#__NEXT_DATA__")
.data()
.substringAfter(":")
.substringBeforeLast(",\"page\"")
return json.decodeFromString<PagePropDto<T>>(nextData).data
}
private inline fun <reified T> Response.parseAs(): T {
val responseBody = body?.string().orEmpty()
return json.decodeFromString(responseBody)
}
private fun String.ifNotEmpty(block: (String) -> String): String {
return if (isNotEmpty() && this != "0") block(this) else ""
}
fun AnimeDataDto.toSAnime(): SAnime {
return SAnime.create().apply {
val ismovie = slug == ""
url = if (ismovie) "/f/$id" else "/anime/$slug"
thumbnail_url = "https://static.anroll.net/images/".let {
if (ismovie) it + "filmes/capas/$slug_movie.jpg"
else it + "animes/capas/$slug.jpg"
}
title = anititle
}
}
companion object {
private const val NEW_API_URL = "https://apiv2-prd.anroll.net"
const val PREFIX_SEARCH = "path:"
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll
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://www.anroll.net/<type>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class AnimesROLLUrlActivity : 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 path = "${pathSegments[0]}/${pathSegments[1]}"
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${AnimesROLL.PREFIX_SEARCH}$path")
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,61 @@
package eu.kanade.tachiyomi.animeextension.pt.animesroll.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class PagePropDto<T>(val pageProps: DataPropDto<T>) {
val data by lazy { pageProps.data }
}
@Serializable
data class DataPropDto<T>(val data: T)
@Serializable
data class LatestAnimeDto(
@SerialName("data_releases")
val animes: List<AnimeDataDto>
)
@Serializable
data class AnimeInfoDto(
@JsonNames("data_anime", "data_movie")
val animeData: AnimeDataDto,
val pages: Int = 0
)
@Serializable
data class AnimeDataDto(
@SerialName("diretor")
val director: String = "",
@JsonNames("nome_filme", "titulo")
val anititle: String,
@JsonNames("sinopse", "sinopse_filme")
val description: String = "",
@SerialName("slug_serie")
val slug: String = "",
@SerialName("slug_filme")
val slug_movie: String = "",
@SerialName("duracao")
val duration: String = "",
@SerialName("generate_id")
val id: String = "",
val animeCalendar: String? = null,
val od: String = ""
)
@Serializable
data class EpisodeDto(
@SerialName("data_anime")
val anime: AnimeDataDto,
val total_ep: Int
)
@Serializable
data class SearchResultsDto(
@SerialName("data_anime")
val animes: List<AnimeDataDto>,
@SerialName("data_filme")
val movies: List<AnimeDataDto>
)