-
Notifications
You must be signed in to change notification settings - Fork 261
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(src/pt): New source: Animes CX (#3285)
Co-authored-by: Secozzi <[email protected]>
- Loading branch information
1 parent
b4f7128
commit 0beabc5
Showing
9 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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=".pt.animescx.AnimesCXUrlActivity" | ||
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="animescx.com.br" | ||
android:pathPattern="/..*/..*" | ||
android:scheme="https" /> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
</manifest> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
ext { | ||
extName = 'Animes CX' | ||
extClass = '.AnimesCX' | ||
extVersionCode = 1 | ||
isNsfw = true | ||
} | ||
|
||
apply from: "$rootDir/common.gradle" | ||
|
||
dependencies { | ||
implementation(project(":lib:googledrive-extractor")) | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
270 changes: 270 additions & 0 deletions
270
src/pt/animescx/src/eu/kanade/tachiyomi/animeextension/pt/animescx/AnimesCX.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
package eu.kanade.tachiyomi.animeextension.pt.animescx | ||
|
||
import android.app.Application | ||
import android.util.Base64 | ||
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.Video | ||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource | ||
import eu.kanade.tachiyomi.lib.googledriveextractor.GoogleDriveExtractor | ||
import eu.kanade.tachiyomi.network.GET | ||
import eu.kanade.tachiyomi.network.await | ||
import eu.kanade.tachiyomi.network.awaitSuccess | ||
import eu.kanade.tachiyomi.util.asJsoup | ||
import eu.kanade.tachiyomi.util.parseAs | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.builtins.serializer | ||
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
import kotlinx.serialization.json.JsonArrayBuilder | ||
import kotlinx.serialization.json.add | ||
import kotlinx.serialization.json.buildJsonObject | ||
import kotlinx.serialization.json.putJsonArray | ||
import okhttp3.Response | ||
import org.jsoup.nodes.Document | ||
import org.jsoup.nodes.Element | ||
import uy.kohesive.injekt.Injekt | ||
import uy.kohesive.injekt.api.get | ||
import uy.kohesive.injekt.injectLazy | ||
|
||
class AnimesCX : ParsedAnimeHttpSource(), ConfigurableAnimeSource { | ||
|
||
override val name = "Animes CX" | ||
|
||
override val baseUrl = "https://animescx.com.br" | ||
|
||
override val lang = "pt-BR" | ||
|
||
override val supportsLatest = true | ||
|
||
private val json: Json by injectLazy() | ||
|
||
private val preferences by lazy { | ||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||
} | ||
|
||
// ============================== Popular =============================== | ||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/doramas-legendados/page/$page", headers) | ||
|
||
override fun popularAnimeParse(response: Response): AnimesPage { | ||
val doc = response.asJsoup() | ||
val animes = doc.select(popularAnimeSelector()).map(::popularAnimeFromElement) | ||
|
||
return AnimesPage(animes, doc.hasNextPage()) | ||
} | ||
|
||
override fun popularAnimeSelector() = "div.listaAnimes_Riverlab_Container > a" | ||
|
||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { | ||
setUrlWithoutDomain(element.attr("href")) | ||
title = element.selectFirst("div.infolistaAnimes_RiverLab")!!.text() | ||
thumbnail_url = element.selectFirst("img")?.absUrl("src") | ||
} | ||
|
||
override fun popularAnimeNextPageSelector(): String? { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
// =============================== Latest =============================== | ||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/doramas-em-lancamento/page/$page", headers) | ||
|
||
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response) | ||
|
||
override fun latestUpdatesSelector(): String { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
override fun latestUpdatesFromElement(element: Element): SAnime { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
override fun latestUpdatesNextPageSelector(): String? { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
// =============================== Search =============================== | ||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage { | ||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler | ||
val path = query.removePrefix(PREFIX_SEARCH) | ||
client.newCall(GET("$baseUrl/$path", headers)) | ||
.awaitSuccess() | ||
.use(::searchAnimeByIdParse) | ||
} else { | ||
super.getSearchAnime(page, query, filters) | ||
} | ||
} | ||
|
||
private fun searchAnimeByIdParse(response: Response): AnimesPage { | ||
val details = animeDetailsParse(response.asJsoup()) | ||
.apply { setUrlWithoutDomain(response.request.url.toString()) } | ||
return AnimesPage(listOf(details), false) | ||
} | ||
|
||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = | ||
GET("$baseUrl/page/$page/?s=$query", headers) | ||
|
||
override fun searchAnimeSelector() = "article.rl_episodios:has(.rl_AnimeIndexImg)" | ||
|
||
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply { | ||
with(element.selectFirst("a")!!) { | ||
setUrlWithoutDomain(attr("href")) | ||
title = text() | ||
} | ||
|
||
thumbnail_url = element.selectFirst("img")?.absUrl("src") | ||
} | ||
|
||
override fun searchAnimeNextPageSelector() = "a.next.page-numbers" | ||
|
||
// =========================== Anime Details ============================ | ||
override fun animeDetailsParse(document: Document) = SAnime.create().apply { | ||
val infos = document.selectFirst("div.rl_anime_metadados")!! | ||
thumbnail_url = infos.selectFirst("img")?.absUrl("src") | ||
title = infos.selectFirst(".rl_nome_anime")!!.text() | ||
|
||
genre = infos.getInfo("Gêneros").replace(";", ",") | ||
status = when (infos.getInfo("Status")) { | ||
"Completo" -> SAnime.COMPLETED | ||
"Lançando", "Sendo Legendado!" -> SAnime.ONGOING | ||
else -> SAnime.UNKNOWN | ||
} | ||
|
||
description = infos.getInfo("Sinopse") | ||
} | ||
|
||
private fun Element.getInfo(text: String) = | ||
selectFirst(".rl_anime_meta:contains($text)")?.ownText().orEmpty() | ||
|
||
// ============================== Episodes ============================== | ||
override fun episodeListSelector() = ".rl_anime_episodios > article.rl_episodios" | ||
|
||
override fun episodeListParse(response: Response) = buildList { | ||
var doc = response.asJsoup() | ||
|
||
do { | ||
if (isNotEmpty()) { | ||
val url = doc.selectFirst("a.rl_anime_pagination:contains(›)")!!.absUrl("href") | ||
doc = client.newCall(GET(url, headers)).execute().asJsoup() | ||
} | ||
|
||
doc.select(episodeListSelector()) | ||
.map(::episodeFromElement) | ||
.also(::addAll) | ||
} while (doc.hasNextPage()) | ||
|
||
reverse() | ||
} | ||
|
||
override fun episodeFromElement(element: Element) = SEpisode.create().apply { | ||
val num = element.selectFirst("header")!!.text().substringAfterLast(' ') | ||
episode_number = num.toFloatOrNull() ?: 0F | ||
name = "Episódio $num" | ||
scanlator = element.selectFirst("div.rl_episodios_info:contains(Fansub)")?.ownText() | ||
|
||
url = json.encodeToString( | ||
buildJsonObject { | ||
element.select("div.rl_episodios_opcnome[onclick]").forEach { | ||
putJsonArray(it.text(), { getVideoHosts(it.attr("onclick"), element) }) | ||
} | ||
}, | ||
) | ||
} | ||
|
||
private fun JsonArrayBuilder.getVideoHosts(onclick: String, element: Element) { | ||
val itemId = onclick.substringAfterLast("rlToggle('").substringBefore("'") | ||
element.select("#$itemId a.rl_episodios_link").toList() | ||
.filter { it.text() != "Mega" } | ||
.forEach { el -> | ||
val urlId = el.attr("href").substringAfter("id=") | ||
val url = String(Base64.decode(urlId, Base64.DEFAULT)).reversed() | ||
add(json.encodeToJsonElement(VideoHost.serializer(), VideoHost(el.text(), url))) | ||
} | ||
} | ||
|
||
@Serializable | ||
class VideoHost(val name: String, val url: String) | ||
|
||
// ============================ Video Links ============================= | ||
private val gdriveExtractor by lazy { GoogleDriveExtractor(client, headers) } | ||
|
||
override suspend fun getVideoList(episode: SEpisode): List<Video> { | ||
val data = episode.url.parseAs<Map<String, List<VideoHost>>>() | ||
|
||
return data.flatMap { (quality, items) -> | ||
items.flatMap { | ||
when (it.name) { | ||
"MediaFire" -> { | ||
val doc = client.newCall(GET(it.url, headers)).await().asJsoup() | ||
val url = doc.selectFirst("a#downloadButton")?.attr("href") | ||
url?.let { listOf(Video(url, "Mediafire - $quality", url, headers)) }.orEmpty() | ||
} | ||
"Google Drive" -> { | ||
GDRIVE_REGEX.find(it.url)?.groupValues?.get(0) | ||
?.let { gdriveExtractor.videosFromUrl(it, "GDrive - $quality") } | ||
.orEmpty() | ||
} | ||
else -> emptyList() | ||
} | ||
} | ||
}.sort() | ||
} | ||
override fun videoListParse(response: Response): List<Video> { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
override fun videoListSelector(): String { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
override fun videoFromElement(element: Element): Video { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
override fun videoUrlParse(document: Document): String { | ||
throw UnsupportedOperationException() | ||
} | ||
|
||
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" | ||
}.also(screen::addPreference) | ||
} | ||
|
||
// ============================= Utilities ============================== | ||
private fun String.getPage() = substringAfterLast("/page/").substringBefore("/") | ||
|
||
private fun Document.hasNextPage() = | ||
selectFirst("a.rl_anime_pagination:last-child") | ||
?.let { it.attr("href").getPage() != location().getPage() } | ||
?: false | ||
|
||
companion object { | ||
const val PREFIX_SEARCH = "id:" | ||
|
||
private val GDRIVE_REGEX = Regex("""[\w-]{28,}""") | ||
|
||
private const val PREF_QUALITY_KEY = "pref_quality_key" | ||
private const val PREF_QUALITY_TITLE = "Qualidade preferida" | ||
private const val PREF_QUALITY_DEFAULT = "FULL HD" | ||
private val PREF_QUALITY_ENTRIES = arrayOf("MP4", "SD", "HD", "FULL HD") | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
src/pt/animescx/src/eu/kanade/tachiyomi/animeextension/pt/animescx/AnimesCXUrlActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package eu.kanade.tachiyomi.animeextension.pt.animescx | ||
|
||
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://animescx.com.br/<type>/<item> intents | ||
* and redirects them to the main Aniyomi process. | ||
*/ | ||
class AnimesCXUrlActivity : 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.joinToString("/") | ||
val mainIntent = Intent().apply { | ||
action = "eu.kanade.tachiyomi.ANIMESEARCH" | ||
putExtra("query", "${AnimesCX.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) | ||
} | ||
} |