chore: Sync with latest commits #5

Merged
WebDitto merged 1 commit from sync into main 2024-06-24 00:06:32 -05:00
115 changed files with 2230 additions and 445 deletions

View file

@ -3,11 +3,13 @@ package eu.kanade.tachiyomi.lib.vidbomextractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.OkHttpClient
class VidBomExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String): List<Video> {
val doc = client.newCall(GET(url)).execute().asJsoup()
fun videosFromUrl(url: String, headers: Headers? = null): List<Video> {
val request = if (headers != null) GET(url, headers) else GET(url)
val doc = client.newCall(request).execute().asJsoup()
val script = doc.selectFirst("script:containsData(sources)")!!
val data = script.data().substringAfter("sources: [").substringBefore("],")

View file

@ -1,7 +1,8 @@
ext {
extName = 'Sudatchi'
extClass = '.Sudatchi'
extVersionCode = 1
extVersionCode = 3
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View file

@ -4,12 +4,13 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.AnimeDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.AnimePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.DirectoryDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomeListDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.LongAnimeDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.ShortAnimeDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.EpisodePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomePageDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.PropsDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.SubtitleDto
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.WatchDto
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -27,6 +28,7 @@ import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -37,6 +39,8 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl = "https://sudatchi.com"
private val ipfsUrl = "https://ipfs.animeui.com"
override val lang = "all"
override val supportsLatest = true
@ -52,7 +56,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/api/home-list", headers)
override fun popularAnimeRequest(page: Int) = GET(baseUrl, headers)
private fun Int.parseStatus() = when (this) {
1 -> SAnime.UNKNOWN // Not Yet Released
@ -61,7 +65,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
else -> SAnime.UNKNOWN
}
private fun ShortAnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
private fun AnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
url = "/anime/$slug"
title = when (titleLang) {
"romaji" -> titleRomanji
@ -70,14 +74,19 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
description = synopsis
status = statusId.parseStatus()
thumbnail_url = "$baseUrl$imgUrl"
thumbnail_url = when {
imgUrl.startsWith("/") -> "$baseUrl$imgUrl"
else -> "$ipfsUrl/ipfs/$imgUrl"
}
genre = animeGenres?.joinToString { it.genre.name }
}
override fun popularAnimeParse(response: Response): AnimesPage {
sudatchiFilters.fetchFilters()
val titleLang = preferences.title
return AnimesPage(response.parseAs<HomeListDto>().animeSpotlight.map { it.toSAnime(titleLang) }, false)
val document = response.asJsoup()
val data = document.parseAs<HomePageDto>().animeSpotlight
return AnimesPage(data.map { it.toSAnime(titleLang) }, false)
}
// =============================== Latest ===============================
@ -97,7 +106,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
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/api/anime/$id", headers))
client.newCall(GET("$baseUrl/anime/$id", headers))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
@ -105,7 +114,14 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
}
}
private fun searchAnimeByIdParse(response: Response) = AnimesPage(listOf(animeDetailsParse(response)), false)
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/api/directory".toHttpUrl().newBuilder()
@ -123,15 +139,18 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
// =========================== Anime Details ============================
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api${anime.url}", headers)
override fun animeDetailsParse(response: Response) = response.parseAs<ShortAnimeDto>().toSAnime(preferences.title)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
val data = document.parseAs<AnimePageDto>().animeData
return data.toSAnime(preferences.title)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
override fun episodeListParse(response: Response): List<SEpisode> {
val anime = response.parseAs<LongAnimeDto>()
val document = response.asJsoup()
val anime = document.parseAs<AnimePageDto>().animeData
return anime.episodes.map {
SEpisode.create().apply {
name = it.title
@ -148,8 +167,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
val jsonString = document.selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList()
val data = json.decodeFromString<WatchDto>(jsonString).props.pageProps.episodeData
val data = document.parseAs<EpisodePageDto>().episodeData
val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson)
// val videoUrl = client.newCall(GET("$baseUrl/api/streams?episodeId=${data.episode.id}", headers)).execute().parseAs<StreamsDto>().url
// keeping it in case the simpler solution breaks, can be hardcoded to this for now :
@ -158,7 +176,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
videoUrl,
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
subtitleList = subtitles.map {
Track("$baseUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
}.sort(),
)
}
@ -226,6 +244,11 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
}
// ============================= Utilities ==============================
private inline fun <reified T> Document.parseAs(): T {
val nextData = this.selectFirst("script#__NEXT_DATA__")!!.data()
return json.decodeFromString<PropsDto<T>>(nextData).props.pageProps
}
private val SharedPreferences.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_DEFAULT)!!
private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!

View file

@ -4,16 +4,23 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Genre(val name: String)
data class GenreDto(val name: String)
@Serializable
data class AnimeGenreRelation(
data class AnimeGenreRelationDto(
@SerialName("Genre")
val genre: Genre,
val genre: GenreDto,
)
@Serializable
data class ShortAnimeDto(
data class EpisodeDto(
val title: String,
val id: Int,
val number: Int,
)
@Serializable
data class AnimeDto(
val titleRomanji: String?,
val titleEnglish: String?,
val titleJapanese: String?,
@ -22,36 +29,46 @@ data class ShortAnimeDto(
val statusId: Int,
val imgUrl: String,
@SerialName("AnimeGenres")
val animeGenres: List<AnimeGenreRelation>?,
val animeGenres: List<AnimeGenreRelationDto>?,
@SerialName("Episodes")
val episodes: List<EpisodeDto> = emptyList(),
)
@Serializable
data class HomeListDto(
data class HomePageDto(
@SerialName("AnimeSpotlight")
val animeSpotlight: List<ShortAnimeDto>,
val animeSpotlight: List<AnimeDto>,
)
@Serializable
data class AnimePageDto(
val animeData: AnimeDto,
)
@Serializable
data class EpisodeDataDto(
val episode: EpisodeDto,
val subtitlesJson: String,
)
@Serializable
data class EpisodePageDto(
val episodeData: EpisodeDataDto,
)
@Serializable
data class PagePropsDto<T>(val pageProps: T)
@Serializable
data class PropsDto<T>(val props: PagePropsDto<T>)
@Serializable
data class DirectoryDto(
val animes: List<ShortAnimeDto>,
val animes: List<AnimeDto>,
val page: Int,
val pages: Int,
)
@Serializable
data class Episode(
val title: String,
val id: Int,
val number: Int,
)
@Serializable
data class LongAnimeDto(
val slug: String,
@SerialName("Episodes")
val episodes: List<Episode>,
)
@Serializable
data class SubtitleLangDto(
val name: String,
@ -65,27 +82,6 @@ data class SubtitleDto(
val subtitlesName: SubtitleLangDto,
)
@Serializable
data class EpisodeDataDto(
val episode: Episode,
val subtitlesJson: String,
)
@Serializable
data class PagePropsDto(
val episodeData: EpisodeDataDto,
)
@Serializable
data class DataWatchDto(
val pageProps: PagePropsDto,
)
@Serializable
data class WatchDto(
val props: DataWatchDto,
)
@Serializable
data class FilterItemDto(
val id: Int,

View file

@ -1,7 +1,7 @@
ext {
extName = 'SupJav'
extClass = '.SupJavFactory'
extVersionCode = 8
extVersionCode = 9
isNsfw = true
}
@ -12,4 +12,4 @@ dependencies {
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:voe-extractor"))
implementation(project(":lib:playlist-utils"))
}
}

View file

@ -93,7 +93,11 @@ class SupJav(override val lang: String = "en") : ConfigurableAnimeSource, Parsed
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

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=".all.torrentio.TorrentioUrlActivity"
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="torrentio.strem.fun"
android:pathPattern="/anime/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,8 @@
ext {
extName = 'Torrentio (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 1
containsNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1,867 @@
package eu.kanade.tachiyomi.animeextension.all.torrentio
import android.app.Application
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.animeextension.all.torrentio.dto.EpisodeList
import eu.kanade.tachiyomi.animeextension.all.torrentio.dto.GetPopularTitlesResponse
import eu.kanade.tachiyomi.animeextension.all.torrentio.dto.GetUrlTitleDetailsResponse
import eu.kanade.tachiyomi.animeextension.all.torrentio.dto.StreamDataTorrent
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.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Torrentio (Torrent / Debrid)"
override val baseUrl = "https://torrentio.strem.fun"
override val lang = "all"
override val supportsLatest = false
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
// ============================== JustWatch API Request ===================
private fun makeGraphQLRequest(query: String, variables: String): Request {
val requestBody = """
{"query": "${query.replace("\n", "")}", "variables": $variables}
""".trimIndent().toRequestBody("application/json; charset=utf-8".toMediaType())
return POST("https://apis.justwatch.com/graphql", headers = headers, body = requestBody)
}
// ============================== JustWatch Api Query ======================
private fun justWatchQuery(): String {
return """
query GetPopularTitles(
${"$"}country: Country!,
${"$"}first: Int!,
${"$"}language: Language!,
${"$"}offset: Int,
${"$"}searchQuery: String,
${"$"}packages: [String!]!,
${"$"}objectTypes: [ObjectType!]!,
${"$"}popularTitlesSortBy: PopularTitlesSorting!,
${"$"}releaseYear: IntFilter
) {
popularTitles(
country: ${"$"}country
first: ${"$"}first
offset: ${"$"}offset
sortBy: ${"$"}popularTitlesSortBy
filter: {
objectTypes: ${"$"}objectTypes,
searchQuery: ${"$"}searchQuery,
packages: ${"$"}packages,
genres: [],
excludeGenres: [],
releaseYear: ${"$"}releaseYear
}
) {
edges {
node {
id
objectType
content(country: ${"$"}country, language: ${"$"}language) {
fullPath
title
shortDescription
externalIds {
imdbId
}
posterUrl
genres {
translation(language: ${"$"}language)
}
credits {
name
role
}
}
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
""".trimIndent()
}
private fun parseSearchJson(jsonLine: String?): AnimesPage {
val jsonData = jsonLine ?: return AnimesPage(emptyList(), false)
val popularTitlesResponse = json.decodeFromString<GetPopularTitlesResponse>(jsonData)
val edges = popularTitlesResponse.data?.popularTitles?.edges.orEmpty()
val hasNextPage = popularTitlesResponse.data?.popularTitles?.pageInfo?.hasNextPage ?: false
val metaList = edges
.mapNotNull { edge ->
val node = edge.node ?: return@mapNotNull null
val content = node.content ?: return@mapNotNull null
SAnime.create().apply {
url = "${content.externalIds?.imdbId ?: ""},${node.objectType ?: ""},${content.fullPath ?: ""}"
title = content.title ?: ""
thumbnail_url = "https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}"
description = content.shortDescription ?: ""
val genresList = content.genres?.mapNotNull { it.translation }.orEmpty()
genre = genresList.joinToString()
val directors = content.credits?.filter { it.role == "DIRECTOR" }?.mapNotNull { it.name }
author = directors?.joinToString()
val actors = content.credits?.filter { it.role == "ACTOR" }?.take(4)?.mapNotNull { it.name }
artist = actors?.joinToString()
initialized = true
}
}
return AnimesPage(metaList, hasNextPage)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
return searchAnimeRequest(page, "", AnimeFilterList())
}
override fun popularAnimeParse(response: Response): AnimesPage {
val jsonData = response.body.string()
return parseSearchJson(jsonData) }
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
// =============================== Search ===============================
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/anime/$id", headers))
.awaitSuccess()
.use(::searchAnimeByIdParse)
} else {
super.getSearchAnime(page, query, filters)
}
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT)
val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT)
val perPage = 40
val packages = ""
val year = 0
val objectTypes = ""
val variables = """
{
"first": $perPage,
"offset": ${(page - 1) * perPage},
"platform": "WEB",
"country": "$country",
"language": "$language",
"searchQuery": "${query.replace(searchQueryRegex, "").trim()}",
"packages": [$packages],
"objectTypes": [$objectTypes],
"popularTitlesSortBy": "TRENDING",
"releaseYear": {
"min": $year,
"max": $year
}
}
""".trimIndent()
return makeGraphQLRequest(justWatchQuery(), variables)
}
private val searchQueryRegex by lazy {
Regex("[^A-Za-z0-9 ]")
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
// =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
// override suspend fun getAnimeDetails(anime: SAnime): SAnime = throw UnsupportedOperationException()
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
val query = """
query GetUrlTitleDetails(${"$"}fullPath: String!, ${"$"}country: Country!, ${"$"}language: Language!) {
urlV2(fullPath: ${"$"}fullPath) {
node {
...TitleDetails
}
}
}
fragment TitleDetails on Node {
... on MovieOrShowOrSeason {
id
objectType
content(country: ${"$"}country, language: ${"$"}language) {
title
shortDescription
externalIds {
imdbId
}
posterUrl
genres {
translation(language: ${"$"}language)
}
}
}
}
""".trimIndent()
val country = preferences.getString(PREF_REGION_KEY, PREF_REGION_DEFAULT)
val language = preferences.getString(PREF_JW_LANG_KEY, PREF_JW_LANG_DEFAULT)
val variables = """
{
"fullPath": "${anime.url.split(',').last()}",
"country": "$country",
"language": "$language"
}
""".trimIndent()
val content = runCatching {
json.decodeFromString<GetUrlTitleDetailsResponse>(client.newCall(makeGraphQLRequest(query, variables)).execute().body.string())
}.getOrNull()?.data?.urlV2?.node?.content
anime.title = content?.title ?: ""
anime.thumbnail_url = "https://images.justwatch.com${content?.posterUrl?.replace("{profile}", "s718")?.replace("{format}", "webp")}"
anime.description = content?.shortDescription ?: ""
val genresList = content?.genres?.mapNotNull { it.translation }.orEmpty()
anime.genre = genresList.joinToString()
return anime
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val parts = anime.url.split(",")
val type = parts[1].lowercase()
val imdbId = parts[0]
return GET("https://cinemeta-live.strem.io/meta/$type/$imdbId.json")
}
override fun episodeListParse(response: Response): List<SEpisode> {
val responseString = response.body.string()
val episodeList = json.decodeFromString<EpisodeList>(responseString)
return when (episodeList.meta?.type) {
"show" -> {
episodeList.meta.videos
?.let { videos ->
if (preferences.getBoolean(UPCOMING_EP_KEY, UPCOMING_EP_DEFAULT)) { videos } else { videos.filter { video -> (video.firstAired?.let { parseDate(it) } ?: 0L) <= System.currentTimeMillis() } }
}
?.map { video ->
SEpisode.create().apply {
episode_number = "${video.season}.${video.number}".toFloat()
url = "/stream/series/${video.id}.json"
date_upload = video.firstAired?.let { parseDate(it) } ?: 0L
name = "S${video.season.toString().trim()}:E${video.number} - ${video.name}"
scanlator = (video.firstAired?.let { parseDate(it) } ?: 0L)
.takeIf { it > System.currentTimeMillis() }
?.let { "Upcoming" }
?: ""
}
}
?.sortedWith(
compareBy<SEpisode> { it.name.substringAfter("S").substringBefore(":").toInt() }
.thenBy { it.name.substringAfter("E").substringBefore(" -").toInt() },
)
.orEmpty().reversed()
}
"movie" -> {
// Handle movie response
listOf(
SEpisode.create().apply {
episode_number = 1.0F
url = "/stream/movie/${episodeList.meta.id}.json"
name = "Movie"
},
).reversed()
}
else -> emptyList()
}
}
private fun parseDate(dateStr: String): Long {
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val mainURL = buildString {
append("$baseUrl/")
val appendQueryParam: (String, Set<String>?) -> Unit = { key, values ->
values?.takeIf { it.isNotEmpty() }?.let {
append("$key=${it.filter(String::isNotBlank).joinToString(",")}|")
}
}
appendQueryParam("providers", preferences.getStringSet(PREF_PROVIDER_KEY, PREF_PROVIDERS_DEFAULT))
appendQueryParam("language", preferences.getStringSet(PREF_LANG_KEY, PREF_LANG_DEFAULT))
appendQueryParam("qualityfilter", preferences.getStringSet(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT))
val sortKey = preferences.getString(PREF_SORT_KEY, "quality")
appendQueryParam("sort", sortKey?.let { setOf(it) })
val token = preferences.getString(PREF_TOKEN_KEY, null)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "none")
when {
token.isNullOrBlank() && debridProvider != "none" -> {
handler.post {
context.let {
Toast.makeText(
it,
"Kindly input the debrid token in the extension settings.",
Toast.LENGTH_LONG,
).show()
}
}
throw UnsupportedOperationException()
}
!token.isNullOrBlank() && debridProvider != "none" -> append("$debridProvider=$token|")
}
append(episode.url)
}.removeSuffix("|")
return GET(mainURL)
}
override fun videoListParse(response: Response): List<Video> {
val responseString = response.body.string()
val streamList = json.decodeFromString<StreamDataTorrent>(responseString)
val debridProvider = preferences.getString(PREF_DEBRID_KEY, "none")
val animeTrackers = """http://nyaa.tracker.wf:7777/announce,
http://anidex.moe:6969/announce,http://tracker.anirena.com:80/announce,
udp://tracker.uw0.xyz:6969/announce,
http://share.camoe.cn:8080/announce,
http://t.nyaatracker.com:80/announce,
udp://47.ip-51-68-199.eu:6969/announce,
udp://9.rarbg.me:2940,
udp://9.rarbg.to:2820,
udp://exodus.desync.com:6969/announce,
udp://explodie.org:6969/announce,
udp://ipv4.tracker.harry.lu:80/announce,
udp://open.stealth.si:80/announce,
udp://opentor.org:2710/announce,
udp://opentracker.i2p.rocks:6969/announce,
udp://retracker.lanta-net.ru:2710/announce,
udp://tracker.cyberia.is:6969/announce,
udp://tracker.dler.org:6969/announce,
udp://tracker.ds.is:6969/announce,
udp://tracker.internetwarriors.net:1337,
udp://tracker.openbittorrent.com:6969/announce,
udp://tracker.opentrackr.org:1337/announce,
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
""".trimIndent()
return streamList.streams?.map { stream ->
val urlOrHash =
if (debridProvider == "none") {
val trackerList = animeTrackers.split(",").map { it.trim() }.filter { it.isNotBlank() }.joinToString("&tr=")
"magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=$trackerList&index=${stream.fileIdx}"
} else stream.url ?: ""
Video(urlOrHash, ((stream.name?.replace("Torrentio\n", "") ?: "") + "\n" + stream.title), urlOrHash)
}.orEmpty()
}
override fun List<Video>.sort(): List<Video> {
val isDub = preferences.getBoolean(IS_DUB_KEY, IS_DUB_DEFAULT)
val isEfficient = preferences.getBoolean(IS_EFFICIENT_KEY, IS_EFFICIENT_DEFAULT)
return sortedWith(
compareBy(
{ Regex("\\[(.+?) download]").containsMatchIn(it.quality) },
{ isDub && !it.quality.contains("dubbed", true) },
{ isEfficient && !arrayOf("hevc", "265", "av1").any { q -> it.quality.contains(q, true) } },
),
)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Debrid provider
ListPreference(screen.context).apply {
key = PREF_DEBRID_KEY
title = "Debrid Provider"
entries = PREF_DEBRID_ENTRIES
entryValues = PREF_DEBRID_VALUES
setDefaultValue("none")
summary = "Choose 'None' for Torrent. If you select a Debrid provider, enter your token key. No token key is needed if 'None' is selected."
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)
// Token
EditTextPreference(screen.context).apply {
key = PREF_TOKEN_KEY
title = "Token"
setDefaultValue(PREF_TOKEN_DEFAULT)
summary = PREF_TOKEN_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
runCatching {
val value = (newValue as String).trim().ifBlank { PREF_TOKEN_DEFAULT }
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
preferences.edit().putString(key, value).commit()
}.getOrDefault(false)
}
}.also(screen::addPreference)
// Provider
MultiSelectListPreference(screen.context).apply {
key = PREF_PROVIDER_KEY
title = "Enable/Disable Providers"
entries = PREF_PROVIDERS
entryValues = PREF_PROVIDERS_VALUE
setDefaultValue(PREF_PROVIDERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
// Exclude Qualities
MultiSelectListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Exclude Qualities/Resolutions"
entries = PREF_QUALITY
entryValues = PREF_QUALITY_VALUE
setDefaultValue(PREF_QUALITY_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
// Priority foreign language
MultiSelectListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = "Priority foreign language"
entries = PREF_LANG
entryValues = PREF_LANG_VALUE
setDefaultValue(PREF_LANG_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
// Sorting
ListPreference(screen.context).apply {
key = PREF_SORT_KEY
title = "Sorting"
entries = PREF_SORT_ENTRIES
entryValues = PREF_SORT_VALUES
setDefaultValue("quality")
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)
SwitchPreferenceCompat(screen.context).apply {
key = UPCOMING_EP_KEY
title = "Show Upcoming Episodes"
setDefaultValue(UPCOMING_EP_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = IS_DUB_KEY
title = "Dubbed Video Priority"
setDefaultValue(IS_DUB_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = IS_EFFICIENT_KEY
title = "Efficient Video Priority"
setDefaultValue(IS_EFFICIENT_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
summary = "Codec: (HEVC / x265) & AV1. High-quality video with less data usage."
}.also(screen::addPreference)
// JustWatch Settings
// Region
ListPreference(screen.context).apply {
key = PREF_REGION_KEY
title = "Catalogue Region"
entries = PREF_REGION_ENTRIES
entryValues = PREF_REGION_VALUES
setDefaultValue(PREF_REGION_DEFAULT)
summary = "Region based catalogue recommendation."
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)
// Poster and Titles Language
ListPreference(screen.context).apply {
key = PREF_JW_LANG_KEY
title = "Poster and Titles Language"
entries = PREF_JW_LANG_ENTRIES
entryValues = PREF_JW_LANG_VALUES
setDefaultValue(PREF_JW_LANG_DEFAULT)
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)
}
companion object {
const val PREFIX_SEARCH = "id:"
// Token
private const val PREF_TOKEN_KEY = "token"
private const val PREF_TOKEN_DEFAULT = ""
private const val PREF_TOKEN_SUMMARY = "Exclusive to Debrid providers; not intended for Torrents."
// Debrid
private const val PREF_DEBRID_KEY = "debrid_provider"
private val PREF_DEBRID_ENTRIES = arrayOf(
"None",
"RealDebrid",
"Premiumize",
"AllDebrid",
"DebridLink",
"Offcloud",
)
private val PREF_DEBRID_VALUES = arrayOf(
"none",
"realdebrid",
"premiumize",
"alldebrid",
"debridlink",
"offcloud",
)
// Sort
private const val PREF_SORT_KEY = "sorting_link"
private val PREF_SORT_ENTRIES = arrayOf(
"By quality then seeders",
"By quality then size",
"By seeders",
"By size",
)
private val PREF_SORT_VALUES = arrayOf(
"quality",
"qualitysize",
"seeders",
"size",
)
// Provider
private const val PREF_PROVIDER_KEY = "provider_selection"
private val PREF_PROVIDERS = arrayOf(
"YTS",
"EZTV",
"RARBG",
"1337x",
"ThePirateBay",
"KickassTorrents",
"TorrentGalaxy",
"MagnetDL",
"HorribleSubs",
"NyaaSi",
"TokyoTosho",
"AniDex",
"🇷🇺 Rutor",
"🇷🇺 Rutracker",
"🇵🇹 Comando",
"🇵🇹 BluDV",
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad",
)
private val PREF_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
"rarbg",
"1337x",
"thepiratebay",
"kickasstorrents",
"torrentgalaxy",
"magnetdl",
"horriblesubs",
"nyaasi",
"tokyotosho",
"anidex",
"rutor",
"rutracker",
"comando",
"bludv",
"torrent9",
"mejortorrent",
"cinecalidad",
)
private val PREF_DEFAULT_PROVIDERS_VALUE = arrayOf(
"yts",
"eztv",
"rarbg",
"1337x",
"thepiratebay",
"kickasstorrents",
"torrentgalaxy",
"magnetdl",
"horriblesubs",
"nyaasi",
"tokyotosho",
"anidex",
)
private val PREF_PROVIDERS_DEFAULT = PREF_DEFAULT_PROVIDERS_VALUE.toSet()
// Qualities/Resolutions
private const val PREF_QUALITY_KEY = "quality_selection"
private val PREF_QUALITY = arrayOf(
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"4k",
"1080p",
"720p",
"480p",
"Other (DVDRip/HDRip/BDRip...)",
"Screener",
"Cam",
"Unknown",
)
private val PREF_QUALITY_VALUE = arrayOf(
"brremux",
"hdrall",
"dolbyvision",
"4k",
"1080p",
"720p",
"480p",
"other",
"scr",
"cam",
"unknown",
)
private val PREF_DEFAULT_QUALITY_VALUE = arrayOf(
"720p",
"480p",
"other",
"scr",
"cam",
"unknown",
)
private val PREF_QUALITY_DEFAULT = PREF_DEFAULT_QUALITY_VALUE.toSet()
// Qualities/Resolutions
private const val PREF_LANG_KEY = "lang_selection"
private val PREF_LANG = arrayOf(
"🇯🇵 Japanese",
"🇷🇺 Russian",
"🇮🇹 Italian",
"🇵🇹 Portuguese",
"🇪🇸 Spanish",
"🇲🇽 Latino",
"🇰🇷 Korean",
"🇨🇳 Chinese",
"🇹🇼 Taiwanese",
"🇫🇷 French",
"🇩🇪 German",
"🇳🇱 Dutch",
"🇮🇳 Hindi",
"🇮🇳 Telugu",
"🇮🇳 Tamil",
"🇵🇱 Polish",
"🇱🇹 Lithuanian",
"🇱🇻 Latvian",
"🇪🇪 Estonian",
"🇨🇿 Czech",
"🇸🇰 Slovakian",
"🇸🇮 Slovenian",
"🇭🇺 Hungarian",
"🇷🇴 Romanian",
"🇧🇬 Bulgarian",
"🇷🇸 Serbian",
"🇭🇷 Croatian",
"🇺🇦 Ukrainian",
"🇬🇷 Greek",
"🇩🇰 Danish",
"🇫🇮 Finnish",
"🇸🇪 Swedish",
"🇳🇴 Norwegian",
"🇹🇷 Turkish",
"🇸🇦 Arabic",
"🇮🇷 Persian",
"🇮🇱 Hebrew",
"🇻🇳 Vietnamese",
"🇮🇩 Indonesian",
"🇲🇾 Malay",
"🇹🇭 Thai",
)
private val PREF_LANG_VALUE = arrayOf(
"japanese",
"russian",
"italian",
"portuguese",
"spanish",
"latino",
"korean",
"chinese",
"taiwanese",
"french",
"german",
"dutch",
"hindi",
"telugu",
"tamil",
"polish",
"lithuanian",
"latvian",
"estonian",
"czech",
"slovakian",
"slovenian",
"hungarian",
"romanian",
"bulgarian",
"serbian",
"croatian",
"ukrainian",
"greek",
"danish",
"finnish",
"swedish",
"norwegian",
"turkish",
"arabic",
"persian",
"hebrew",
"vietnamese",
"indonesian",
"malay",
"thai",
)
private val PREF_LANG_DEFAULT = setOf<String>()
private const val UPCOMING_EP_KEY = "upcoming_ep"
private const val UPCOMING_EP_DEFAULT = true
private const val IS_DUB_KEY = "dubbed"
private const val IS_DUB_DEFAULT = false
private const val IS_EFFICIENT_KEY = "efficient"
private const val IS_EFFICIENT_DEFAULT = false
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
}
// JustWatch settings
// Region
private const val PREF_REGION_KEY = "jw_region"
private val PREF_REGION_ENTRIES = arrayOf(
"Albania", "Algeria", "Androrra", "Angola", "Antigua and Barbuda", "Argentina", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Barbados", "Belarus", "Belgium", "Belize", "Bermuda", "Bolivia", "Bosnia and Herzegovina", "Brazil", "Bulgaria", "Burkina Faso", "Cameroon", "Canada", "Cape Verde", "Chad", "Chile", "Colombia", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "DR Congo", "Denmark", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Estonia", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "Germany", "Ghana", "Gibraltar", "Greece", "Guatemala", "Guernsey", "Guyana", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kenya", "Kosovo", "Kuwait", "Latvia", "Lebanon", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Mali", "Malta", "Mauritius", "Mexico", "Moldova", "Monaco", "Montenegro", "Morocco", "Mozambique", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Norway", "Oman", "Pakistan", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Saint Lucia", "San Marino", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Singapore", "Slovakia", "Slovenia", "South Africa", "South Korea", "Spain", "Sweden", "Switzerland", "Taiwan", "Tanzania", "Thailand", "Trinidad and Tobago", "Tunisia", "Turkey", "Turks and Caicos Islands", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Vatican City", "Venezuela", "Yemen", "Zambia", "Zimbabwe",
)
private val PREF_REGION_VALUES = arrayOf(
"AL", "DZ", "AD", "AO", "AG", "AR", "AU", "AT", "AZ", "BS", "BH", "BB", "BY", "BE", "BZ", "BM", "BO", "BA", "BR", "BG", "BF", "CM", "CA", "CV", "TD", "CL", "CO", "CR", "HR", "CU", "CY", "CZ", "CD", "DK", "DO", "EC", "EG", "SV", "GQ", "EE", "FJ", "FI", "FR", "GF", "PF", "DE", "GH", "GI", "GR", "GT", "GG", "GY", "HN", "HK", "HU", "IS", "IN", "ID", "IQ", "IE", "IL", "IT", "CI", "JM", "JP", "JO", "KE", "XK", "KW", "LV", "LB", "LY", "LI", "LT", "LU", "MK", "MG", "MW", "MY", "ML", "MT", "MU", "MX", "MD", "MC", "ME", "MA", "MZ", "NL", "NZ", "NI", "NE", "NG", "NO", "OM", "PK", "PS", "PA", "PG", "PY", "PE", "PH", "PL", "PT", "QA", "RO", "RU", "LC", "SM", "SA", "SN", "RS", "SC", "SG", "SK", "SI", "ZA", "KR", "ES", "SE", "CH", "TW", "TZ", "TH", "TT", "TN", "TR", "TC", "UG", "UA", "AE", "UK", "US", "UY", "VA", "VE", "YE", "ZM", "ZW",
)
private const val PREF_REGION_DEFAULT = "US"
// JustWatch language in Poster, Titles
private const val PREF_JW_LANG_KEY = "jw_lang"
private val PREF_JW_LANG_ENTRIES = arrayOf(
"Arabic", "Azerbaijani", "Belarusian", "Bulgarian", "Bosnian", "Catalan", "Czech", "German", "Greek", "English", "English (U.S.A.)", "Spanish", "Spanish (Spain)", "Spanish (Latinamerican)", "Estonian", "Finnish", "French", "French (Canada)", "Hebrew", "Croatian", "Hungarian", "Icelandic", "Italian", "Japanese", "Korean", "Lithuanian", "Latvian", "Macedonian", "Maltese", "Polish", "Portuguese", "Portuguese (Portugal)", "Portuguese (Brazil)", "Romanian", "Russian", "Slovakian", "Slovenian", "Albanian", "Serbian", "Swedish", "Swahili", "Turkish", "Ukrainian", "Urdu", "Chinese",
)
private val PREF_JW_LANG_VALUES = arrayOf(
"ar", "az", "be", "bg", "bs", "ca", "cs", "de", "el", "en", "en-US", "es", "es-ES", "es-LA", "et", "fi", "fr", "fr-CA", "he", "hr", "hu", "is", "it", "ja", "ko", "lt", "lv", "mk", "mt", "pl", "pt", "pt-PT", "pt-BR", "ro", "ru", "sk", "sl", "sq", "sr", "sv", "sw", "tr", "uk", "ur", "zh",
)
private const val PREF_JW_LANG_DEFAULT = "en"
}
}

View file

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.all.torrentio
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://torrentio.strem.fun/anime/<item> intents
* and redirects them to the main Aniyomi process.
*/
class TorrentioUrlActivity : 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", "${Torrentio.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,115 @@
package eu.kanade.tachiyomi.animeextension.all.torrentio.dto
import kotlinx.serialization.Serializable
@Serializable
class GetPopularTitlesResponse(
val data: PopularTitlesData? = null,
)
@Serializable
class PopularTitlesData(
val popularTitles: PopularTitles? = null,
)
@Serializable
class PopularTitles(
val edges: List<PopularTitlesEdge>? = null,
val pageInfo: PageInfo? = null,
)
@Serializable
class PopularTitlesEdge(
val node: PopularTitleNode? = null,
)
@Serializable
class PopularTitleNode(
val objectType: String? = null,
val content: Content? = null,
)
@Serializable
class Content(
val fullPath: String? = null,
val title: String? = null,
val shortDescription: String? = null,
val externalIds: ExternalIds? = null,
val posterUrl: String? = null,
val genres: List<Genre>? = null,
val credits: List<Credit>? = null,
)
@Serializable
class ExternalIds(
val imdbId: String? = null,
)
@Serializable
class Genre(
val translation: String? = null,
)
@Serializable
class Credit(
val name: String? = null,
val role: String? = null,
)
@Serializable
class PageInfo(
val hasNextPage: Boolean = false,
)
@Serializable
class GetUrlTitleDetailsResponse(
val data: UrlV2Data? = null,
)
@Serializable
class UrlV2Data(
val urlV2: UrlV2? = null,
)
@Serializable
class UrlV2(
val node: PopularTitleNode? = null,
)
// Stream Data For Torrent
@Serializable
class StreamDataTorrent(
val streams: List<TorrentioStream>? = null,
)
@Serializable
class TorrentioStream(
val name: String? = null,
val title: String? = null,
val infoHash: String? = null,
val fileIdx: Int? = null,
val url: String? = null,
)
// Episode Data
@Serializable
class EpisodeList(
val meta: EpisodeMeta? = null,
)
@Serializable
class EpisodeMeta(
val id: String? = null,
val type: String? = null,
val videos: List<EpisodeVideo>? = null,
)
@Serializable
class EpisodeVideo(
val id: String? = null,
val season: Int? = null,
val number: Int? = null,
val firstAired: String? = null,
val name: String? = null,
)

View file

@ -1,7 +1,7 @@
ext {
extName = 'Torrentio Anime (Torrent / Debrid)'
extClass = '.Torrentio'
extVersionCode = 6
extVersionCode = 7
containsNsfw = false
}

View file

@ -225,7 +225,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage {
val jsonData = response.body.string()
return parseSearchJson(jsonData) }
return parseSearchJson(jsonData)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
@ -258,7 +259,11 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}
@ -479,7 +484,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
if (debridProvider == "none") {
val trackerList = animeTrackers.split(",").map { it.trim() }.filter { it.isNotBlank() }.joinToString("&tr=")
"magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=$trackerList&index=${stream.fileIdx}"
} else stream.url ?: ""
} else {
stream.url ?: ""
}
Video(urlOrHash, ((stream.name?.replace("Torrentio\n", "") ?: "") + "\n" + stream.title), urlOrHash)
}.orEmpty()
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime4up'
extClass = '.Anime4Up'
extVersionCode = 55
extVersionCode = 56
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimeLek'
extClass = '.AnimeLek'
extVersionCode = 29
extVersionCode = 30
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animerco'
extClass = '.Animerco'
extVersionCode = 36
extVersionCode = 37
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'asia2tv'
extClass = '.Asia2TV'
extVersionCode = 17
extVersionCode = 18
}
apply from: "$rootDir/common.gradle"

View file

@ -1,11 +1,14 @@
ext {
extName = 'MY CIMA'
extClass = '.MyCima'
extVersionCode = 20
extVersionCode = 22
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:uqload-extractor'))
}
implementation(project(':lib:vidbom-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:okru-extractor'))
}

View file

@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.animeextension.ar.mycima
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.ar.mycima.extractors.GoVadExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
@ -13,7 +11,10 @@ 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.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
@ -23,13 +24,14 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.Exception
class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "MY Cima"
override val baseUrl by lazy { getPrefBaseUrl() }
// TODO: Check frequency of url changes to potentially
// add back overridable baseurl preference
override val baseUrl = "https://wecima.show"
override val lang = "ar"
@ -39,13 +41,14 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== popular ==============================
override fun popularAnimeSelector(): String = "div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
// ============================== Popular ==============================
override fun popularAnimeSelector(): String =
"div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
override fun popularAnimeNextPageSelector(): String = "ul.page-numbers li a.next"
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/seriestv/top/?page_number=$page")
override fun popularAnimeRequest(page: Int): Request =
GET("$baseUrl/seriestv/top/?page_number=$page", headers)
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
@ -59,116 +62,125 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return anime
}
// ============================== episodes ==============================
// ============================== Episodes ==============================
override fun episodeListSelector() = "div.Episodes--Seasons--Episodes a"
private fun seasonsNextPageSelector(seasonNumber: Int) = "div.List--Seasons--Episodes > a:nth-child($seasonNumber)"
private fun seasonsListSelector() = "div.List--Seasons--Episodes a"
override fun episodeListParse(response: Response): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
var seasonNumber = 1
fun addEpisodes(document: Document) {
if (document.select(episodeListSelector()).isNullOrEmpty()) {
if (!document.select("mycima singlerelated.hasdivider ${popularAnimeSelector()}").isNullOrEmpty()) {
document.select("mycima singlerelated.hasdivider ${popularAnimeSelector()}").map { episodes.add(newEpisodeFromElement(it, "mSeries")) }
} else {
episodes.add(newEpisodeFromElement(document.selectFirst("div.Poster--Single-begin > a")!!, "movie"))
}
val document = response.asJsoup()
return if (document.select(episodeListSelector()).isNullOrEmpty()) {
val movieSeries =
document.select("singlerelated.hasdivider:contains(سلسلة) div.Thumb--GridItem a")
if (movieSeries.isNotEmpty()) {
movieSeries.sortedByDescending {
it.selectFirst(".year")!!.text().let(::getNumberFromEpsString)
}.map(::mSeriesEpisode)
} else {
document.select(episodeListSelector()).map { episodes.add(newEpisodeFromElement(it)) }
document.selectFirst(seasonsNextPageSelector(seasonNumber))?.let {
seasonNumber++
addEpisodes(
client.newCall(GET(it.attr("abs:href"), headers)).execute().asJsoup(),
)
document.selectFirst("div.Poster--Single-begin > a")!!.let(::movieEpisode)
}
} else {
val seasonsList = document.select(seasonsListSelector())
if (seasonsList.isNullOrEmpty()) {
document.select(episodeListSelector()).map(::newEpisodeFromElement)
} else {
seasonsList.reversed().flatMap { season ->
val seNum = season.text().let(::getNumberFromEpsString)
if (season.hasClass("selected")) {
document.select(episodeListSelector())
.map { newEpisodeFromElement(it, seNum) }
} else {
val seasonDoc =
client.newCall(GET(season.absUrl("href"), headers)).execute().asJsoup()
seasonDoc.select(episodeListSelector())
.map { newEpisodeFromElement(it, seNum) }
}
}
}
}
addEpisodes(response.asJsoup())
return episodes
}
private fun newEpisodeFromElement(element: Element, type: String = "series"): SEpisode {
private fun movieEpisode(element: Element): List<SEpisode> =
newEpisodeFromElement(element, type = "movie").let(::listOf)
private fun mSeriesEpisode(element: Element): SEpisode =
newEpisodeFromElement(element, type = "mSeries")
private fun newEpisodeFromElement(
element: Element,
seNum: String = "1",
type: String = "series",
): SEpisode {
val episode = SEpisode.create()
val epNum = getNumberFromEpsString(element.text())
episode.setUrlWithoutDomain(if (type == "mSeries") element.select("a").attr("href") else element.attr("abs:href"))
if (type == "series") {
episode.episode_number = when {
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
else -> 1F
}
}
episode.setUrlWithoutDomain(
when (type) {
"series" -> element.select("a").attr("href")
else -> element.absUrl("href")
},
)
episode.name = when (type) {
"movie" -> "مشاهدة"
"mSeries" -> element.select("a").attr("title")
else -> element.ownerDocument()!!.select("div.List--Seasons--Episodes a.selected").text() + element.text()
"series" -> "الموسم $seNum : ${element.text()}"
"mSeries" -> element.text().replace("مشاهدة فيلم ", "").substringBefore("مترجم")
else -> "مشاهدة"
}
episode.episode_number = when (type) {
"series" -> "$seNum.${element.text().let(::getNumberFromEpsString)}".toFloat()
else -> 1F
}
return episode
}
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode =
throw UnsupportedOperationException()
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// ============================== video urls ==============================
private fun getNumberFromEpsString(epsStr: String): String = epsStr.filter { it.isDigit() }
// ============================== Video Links ==============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return document.select("ul.WatchServersList li btn").parallelCatchingFlatMapBlocking {
val frameURL = it.attr("data-url")
if (it.parent()?.hasClass("MyCimaServer") == true) {
val referer = response.request.url.encodedPath
val newHeader = headers.newBuilder().add("referer", baseUrl + referer).build()
val iframeResponse = client.newCall(GET(frameURL, newHeader)).execute().asJsoup()
videosFromElement(iframeResponse.selectFirst(videoListSelector())!!)
} else {
extractVideos(frameURL)
}
}
return document.select(videoListSelector())
.parallelCatchingFlatMapBlocking(::extractVideos)
}
private fun extractVideos(url: String): List<Video> {
private val vidBomExtractor by lazy { VidBomExtractor(client) }
private val uqloadExtractor by lazy { UqloadExtractor(client) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val okruExtractor by lazy { OkruExtractor(client) }
private fun extractVideos(element: Element): List<Video> {
val iframeUrl = element.selectFirst("btn")!!.absUrl("data-url")
val newHeader = headers.newBuilder().add("referer", "$baseUrl/").build()
val iframeTxt = element.text().lowercase()
return when {
GOVAD_REGEX.containsMatchIn(url) -> {
val finalUrl = GOVAD_REGEX.find(url)!!.groupValues[0]
val urlHost = GOVAD_REGEX.find(url)!!.groupValues[1]
GoVadExtractor(client).videosFromUrl("https://www.$finalUrl.html", urlHost)
element.hasClass("MyCimaServer") && "/run/" in iframeUrl -> {
val mp4Url = iframeUrl.replace("?Key", "/?Key") + "&auto=true"
Video(mp4Url, "Default (may take a while)", mp4Url, newHeader).let(::listOf)
}
UQLOAD_REGEX.containsMatchIn(url) -> {
val finalUrl = UQLOAD_REGEX.find(url)!!.groupValues[0]
UqloadExtractor(client).videosFromUrl("https://www.$finalUrl.html")
"govid" in iframeTxt || "vidbom" in iframeTxt || "vidshare" in iframeTxt -> {
vidBomExtractor.videosFromUrl(iframeUrl, newHeader)
}
"dood" in iframeTxt -> {
doodExtractor.videosFromUrl(iframeUrl)
}
"ok.ru" in iframeTxt -> {
okruExtractor.videosFromUrl(iframeUrl)
}
"uqload" in iframeTxt -> {
uqloadExtractor.videosFromUrl(iframeUrl)
}
else -> null
} ?: emptyList()
}
override fun videoListSelector() = "body"
private fun videosFromElement(element: Element): List<Video> {
val videoList = mutableListOf<Video>()
val script = element.select("script")
.firstOrNull { it.data().contains("player.qualityselector({") }
if (script != null) {
val data = element.data().substringAfter("sources: [").substringBefore("],")
val sources = data.split("format: '").drop(1)
for (source in sources) {
val src = source.substringAfter("src: \"").substringBefore("\"")
val quality = source.substringBefore("'") // .substringAfter("format: '")
val video = Video(src, quality, src)
videoList.add(video)
}
return videoList
}
val sourceTag = element.ownerDocument()!!.select("source").firstOrNull()!!
return listOf(Video(sourceTag.attr("src"), "Default", sourceTag.attr("src")))
}
override fun videoListSelector() = "ul.WatchServersList li"
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val quality = preferences.getString("preferred_quality", "1080")!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
@ -178,65 +190,50 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================== search ==============================
// ============================== Search ==============================
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("a > strong").text()
anime.thumbnail_url = element.select("a > span.BG--GridItem").attr("data-lazy-style").substringAfter("-image:url(").substringBefore(");")
return anime
}
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeNextPageSelector(): String = "ul.page-numbers li a.next"
override fun searchAnimeSelector(): String = "div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
if (query.isNotBlank()) {
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is SearchCategoryList -> {
val catQ = getSearchCategoryList()[filter.state].query
val catUrl = "$baseUrl/search/$query/" + if (catQ == "page/" && page == 1) "" else "$catQ$page"
return GET(catUrl, headers)
}
else -> {}
}
}
val filterList = if (filters.isEmpty()) getFilterList() else filters
val sectionFilter = filterList.find { it is SectionFilter } as SectionFilter
val categoryFilter = filterList.find { it is CategoryFilter } as CategoryFilter
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val url = baseUrl + if (query.isNotBlank()) {
"/search/$query/${categoryFilter.toUriPart()}$page/"
} else if (sectionFilter.state != 0) {
"/${sectionFilter.toUriPart()}/page/$page/"
} else {
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is CategoryList -> {
if (filter.state > 0) {
val catQ = getCategoryList()[filter.state].query
val catUrl = "$baseUrl/$catQ/page/$page/"
return GET(catUrl, headers)
}
}
else -> {}
}
}
throw Exception("Choose a Filters")
"/genre/${genreFilter.toUriPart()}/${categoryFilter.toUriPart()}$page/"
}
return GET(baseUrl, headers)
return GET(url, headers)
}
// ============================== details ==============================
// ============================== Details ==============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create()
anime.title = when {
document.selectFirst("li:contains(المسلسل) p") != null -> {
document.select("li:contains(المسلسل) p").text()
}
document.selectFirst("singlerelated.hasdivider:contains(سلسلة) a") != null -> {
document.selectFirst("singlerelated.hasdivider:contains(سلسلة) a")!!.text()
}
else -> {
document.select("div.Title--Content--Single-begin > h1").text().substringBefore(" (")
document.select("div.Title--Content--Single-begin > h1").text()
.substringBefore(" (").replace("مشاهدة فيلم ", "").substringBefore("مترجم")
}
}
anime.genre = document.select("li:contains(التصنيف) > p > a, li:contains(النوع) > p > a").joinToString(", ") { it.text() }
anime.genre = document.select("li:contains(التصنيف) > p > a, li:contains(النوع) > p > a")
.joinToString(", ") { it.text() }
anime.description = document.select("div.AsideContext > div.StoryMovieContent").text()
anime.author = document.select("li:contains(شركات الإنتاج) > p > a").joinToString(", ") { it.text() }
anime.author =
document.select("li:contains(شركات الإنتاج) > p > a").joinToString(", ") { it.text() }
// add alternative name to anime description
document.select("li:contains( بالعربي) > p, li:contains(معروف) > p").text().let {
if (it.isEmpty().not()) {
@ -249,101 +246,111 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return anime
}
// ============================== latest ==============================
// ============================== Latest ==============================
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesSelector(): String = "div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesNextPageSelector(): String = "ul.page-numbers li a.next"
override fun latestUpdatesFromElement(element: Element): SAnime =
popularAnimeFromElement(element)
override fun latestUpdatesFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.select("a").attr("href"))
anime.title = element.select("a > strong").text()
anime.thumbnail_url = element.select("a > span").attr("data-lazy-style").substringAfter("-image:url(").substringBefore(");")
return anime
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page", headers)
// ============================== Filters ==============================
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("هذا القسم يعمل لو كان البحث فارع"),
SectionFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("النوع يستخدم فى البحث و التصنيف"),
CategoryFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("التصنيف يعمل لو كان اقسام الموقع على 'اختر' فقط"),
GenreFilter(),
)
private class CategoryFilter : PairFilter(
"النوع",
arrayOf(
Pair("فيلم", "page/"),
Pair("مسلسل", "list/series/?page_number="),
Pair("انمى", "list/anime/?page_number="),
Pair("برنامج", "list/tv/?page_number="),
),
)
private class SectionFilter : PairFilter(
"اقسام الموقع",
arrayOf(
Pair("اختر", ""),
Pair("جميع الافلام", "movies"),
Pair("افلام اجنبى", "category/أفلام/10-movies-english-افلام-اجنبي"),
Pair("افلام عربى", "category/أفلام/افلام-عربي-arabic-movies"),
Pair("افلام هندى", "category/أفلام/افلام-هندي-indian-movies"),
Pair("افلام تركى", "category/أفلام/افلام-تركى-turkish-films"),
Pair("افلام وثائقية", "category/أفلام/افلام-وثائقية-documentary-films"),
Pair("افلام انمي", "category/افلام-كرتون"),
Pair(
"سلاسل افلام",
"category/أفلام/10-movies-english-افلام-اجنبي/سلاسل-الافلام-الكاملة-full-pack",
),
Pair("مسلسلات", "seriestv"),
Pair("مسلسلات اجنبى", "category/مسلسلات/5-series-english-مسلسلات-اجنبي"),
Pair("مسلسلات عربى", "category/مسلسلات/5-series-english-مسلسلات-اجنبي"),
Pair("مسلسلات هندى", "category/مسلسلات/9-series-indian-مسلسلات-هندية"),
Pair("مسلسلات اسيوى", "category/مسلسلات/مسلسلات-اسيوية"),
Pair("مسلسلات تركى", "category/مسلسلات/8-مسلسلات-تركية-turkish-series"),
Pair("مسلسلات وثائقية", "category/مسلسلات/مسلسلات-وثائقية-documentary-series"),
Pair("مسلسلات انمي", "category/مسلسلات-كرتون"),
Pair("NETFLIX", "production/netflix"),
Pair("WARNER BROS", "production/warner-bros"),
Pair("LIONSGATE", "production/lionsgate"),
Pair("DISNEY", "production/walt-disney-pictures"),
Pair("COLUMBIA", "production/columbia-pictures"),
),
)
private class GenreFilter : PairFilter(
"التصنيف",
arrayOf(
Pair("اكشن", "اكشن-action"),
Pair("مغامرات", "مغامرات-adventure"),
Pair("خيال علمى", "خيال-علمى-science-fiction"),
Pair("فانتازيا", "فانتازيا-fantasy"),
Pair("كوميديا", "كوميديا-comedy"),
Pair("دراما", "دراما-drama"),
Pair("جريمة", "جريمة-crime"),
Pair("اثارة", "اثارة-thriller"),
Pair("رعب", "رعب-horror"),
Pair("سيرة ذاتية", "سيرة-ذاتية-biography"),
Pair("كرتون", "كرتون"),
Pair("انيميشين", "انيميشين-anime"),
),
)
open class PairFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page")
// ============================== filters ==============================
override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("فلترات البحث"),
SearchCategoryList(searchCategoryNames),
AnimeFilter.Separator(),
AnimeFilter.Header("اقسام الموقع (تعمل فقط اذا كان البحث فارغ)"),
CategoryList(categoryNames),
)
private class SearchCategoryList(categories: Array<String>) : AnimeFilter.Select<String>("بحث عن", categories)
private class CategoryList(categories: Array<String>) : AnimeFilter.Select<String>("اختر قسم", categories)
private data class CatUnit(val name: String, val query: String)
private val searchCategoryNames = getSearchCategoryList().map {
it.name
}.toTypedArray()
private val categoryNames = getCategoryList().map {
it.name
}.toTypedArray()
private fun getSearchCategoryList() = listOf(
CatUnit("فيلم", "page/"),
CatUnit("مسلسل", "list/series/?page_number="),
CatUnit("انمى", "list/anime/?page_number="),
CatUnit("برنامج", "list/tv/?page_number="),
)
private fun getCategoryList() = listOf(
CatUnit("اختر", ""),
CatUnit("جميع الافلام", "category/أفلام/"),
CatUnit("افلام اجنبى", "category/أفلام/10-movies-english-افلام-اجنبي"),
CatUnit("افلام عربى", "category/أفلام/افلام-عربي-arabic-movies"),
CatUnit("افلام هندى", "category/أفلام/افلام-هندي-indian-movies"),
CatUnit("افلام تركى", "category/أفلام/افلام-تركى-turkish-films"),
CatUnit("افلام وثائقية", "category/أفلام/افلام-وثائقية-documentary-films"),
CatUnit("افلام انمي", "category/افلام-كرتون"),
CatUnit("سلاسل افلام", "category/أفلام/10-movies-english-افلام-اجنبي/سلاسل-الافلام-الكاملة-full-pack"),
CatUnit("مسلسلات", "category/مسلسلات"),
CatUnit("مسلسلات اجنبى", "category/مسلسلات/5-series-english-مسلسلات-اجنبي"),
CatUnit("مسلسلات عربى", "category/مسلسلات/5-series-english-مسلسلات-اجنبي"),
CatUnit("مسلسلات هندى", "category/مسلسلات/9-series-indian-مسلسلات-هندية"),
CatUnit("مسلسلات اسيوى", "category/مسلسلات/مسلسلات-اسيوية"),
CatUnit("مسلسلات تركى", "category/مسلسلات/8-مسلسلات-تركية-turkish-series"),
CatUnit("مسلسلات وثائقية", "category/مسلسلات/مسلسلات-وثائقية-documentary-series"),
CatUnit("مسلسلات انمي", "category/مسلسلات-كرتون"),
CatUnit("NETFLIX", "production/netflix"),
CatUnit("WARNER BROS", "production/warner-bros"),
CatUnit("LIONSGATE", "production/lionsgate"),
CatUnit("DISNEY", "production/walt-disney-pictures"),
CatUnit("COLUMBIA", "production/columbia-pictures"),
)
// preferred quality settings
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = PREF_BASE_URL_KEY
title = PREF_BASE_URL_TITLE
summary = getPrefBaseUrl()
this.setDefaultValue(PREF_BASE_URL_DEFAULT)
dialogTitle = PREF_BASE_URL_DIALOG_TITLE
dialogMessage = PREF_BASE_URL_DIALOG_MESSAGE
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(PREF_BASE_URL_KEY, newValue as String).commit()
Toast.makeText(screen.context, "Restart Aniyomi to apply changes", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES.map { it.replace("p", "") }.toTypedArray()
setDefaultValue(PREF_QUALITY_DEFAULT)
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf(
"1080p",
"720p",
"480p",
"360p",
"240p",
"Vidbom",
"Vidshare",
"Dood",
"Default",
)
entryValues =
arrayOf("1080", "720", "480", "360", "240", "Vidbom", "Vidshare", "Dood", "Default")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
@ -352,26 +359,6 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(baseUrlPref)
screen.addPreference(videoQualityPref)
}
private fun getPrefBaseUrl(): String = preferences.getString(PREF_BASE_URL_KEY, PREF_BASE_URL_DEFAULT)!!
// ============================= Utilities ===================================
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p", "240p")
private const val PREF_BASE_URL_DEFAULT = "https://cdn3.wecima.watch"
private const val PREF_BASE_URL_KEY = "default_domain"
private const val PREF_BASE_URL_TITLE = "Enter default domain"
private const val PREF_BASE_URL_DIALOG_TITLE = "Default domain"
private const val PREF_BASE_URL_DIALOG_MESSAGE = "You can change the site domain from here"
private val GOVAD_REGEX = Regex("(v[aie]d[bp][aoe]?m|myvii?d|govad|segavid|v[aei]{1,2}dshar[er]?)\\.(?:com|net|org|xyz)(?::\\d+)?/(?:embed[/-])?([A-Za-z0-9]+)")
private val UQLOAD_REGEX = Regex("(uqload\\.[ic]om?)/(?:embed-)?([0-9a-zA-Z]+)")
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Okanime'
extClass = '.Okanime'
extVersionCode = 8
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"
@ -12,4 +12,4 @@ dependencies {
implementation(project(":lib:okru-extractor"))
implementation(project(":lib:vidbom-extractor"))
implementation(project(":lib:mp4upload-extractor"))
}
}

View file

@ -77,7 +77,11 @@ class Okanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Tuktuk Cinema'
extClass = '.Tuktukcinema'
extVersionCode = 21
extVersionCode = 22
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'WIT ANIME'
extClass = '.WitAnime'
extVersionCode = 48
extVersionCode = 49
}
apply from: "$rootDir/common.gradle"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Einfach'
extClass = '.Einfach'
extVersionCode = 9
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"
@ -15,4 +15,4 @@ dependencies {
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:voe-extractor"))
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}
}

View file

@ -87,7 +87,11 @@ class Einfach : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'FMovies'
extClass = '.FMovies'
extVersionCode = 24
extVersionCode = 25
}
apply from: "$rootDir/common.gradle"

View file

@ -37,7 +37,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "FMovies"
override val baseUrl = "https://fmoviesz.to"
override val baseUrl = "https://fmovies24.to"
override val lang = "en"

View file

@ -1,7 +1,7 @@
ext {
extName = 'Hstream'
extClass = '.Hstream'
extVersionCode = 8
extVersionCode = 9
isNsfw = true
}

View file

@ -90,7 +90,10 @@ class Hstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'KickAssAnime'
extClass = '.KickAssAnime'
extVersionCode = 42
extVersionCode = 43
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:cryptoaes"))
implementation(project(":lib:playlist-utils"))
}
}

View file

@ -249,7 +249,11 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'My Running Man'
extClass = '.MyRunningMan'
extVersionCode = 4
extVersionCode = 5
}
apply from: "$rootDir/common.gradle"
@ -10,4 +10,4 @@ dependencies {
implementation(project(":lib:dood-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:streamtape-extractor"))
}
}

View file

@ -73,7 +73,11 @@ class MyRunningMan : ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime-Sama'
extClass = '.AnimeSama'
extVersionCode = 9
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"

View file

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
@ -64,9 +65,13 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Latest ===============================
override fun latestUpdatesParse(response: Response): AnimesPage {
val animes = response.asJsoup()
val seasons = animes.select("h2:contains(derniers ajouts) + .scrollBarStyled > div").flatMap {
val animeUrl = it.getElementsByTag("a").attr("href")
fetchAnimeSeasons(animeUrl)
val seasons = animes.select("#containerAjoutsAnimes > div").flatMap {
val animeUrl = it.getElementsByTag("a").attr("href").toHttpUrl()
val url = animeUrl.newBuilder()
.removePathSegment(animeUrl.pathSize - 2)
.removePathSegment(animeUrl.pathSize - 3)
.build()
fetchAnimeSeasons(url.toString())
}
return AnimesPage(seasons, false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AniSama'
extClass = '.AniSama'
extVersionCode = 3
extVersionCode = 5
}
apply from: "$rootDir/common.gradle"
@ -13,4 +13,4 @@ dependencies {
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:dood-extractor'))
implementation(project(':lib:streamhidevid-extractor'))
}
}

View file

@ -40,7 +40,7 @@ class AniSama : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val name = "AniSama"
override val baseUrl = "https://animesz.xyz"
override val baseUrl = "https://v1.anisama.net"
override val lang = "fr"
@ -97,8 +97,11 @@ class AniSama : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
.apply { setUrlWithoutDomain(response.request.url.toString()) }
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'NekoSama'
extClass = '.NekoSama'
extVersionCode = 11
extVersionCode = 10
isNsfw = true
}

View file

@ -68,16 +68,18 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> {
val pageBody = response.asJsoup()
val episodes = pageBody.select(".episodes a")
val episodesJson = pageBody.selectFirst("script:containsData(var episodes =)")!!.data()
.substringAfter("var episodes = ").substringBefore(";")
val json = json.decodeFromString<List<EpisodesJson>>(episodesJson)
return episodes.map {
return json.map {
SEpisode.create().apply {
name = "Episode " + it.text().substringAfter("-").substringBefore("-").trim()
setUrlWithoutDomain(it.attr("href"))
name = try { it.episode!! } catch (e: Exception) { "episode" }
url = it.url!!.replace("\\", "")
episode_number = it.text().substringAfterLast("-").toFloat()
episode_number = try { it.episode!!.substringAfter(". ").toFloat() } catch (e: Exception) { (0..10).random() }.toFloat()
}
}
}.reversed()
}
override fun episodeListSelector() = throw UnsupportedOperationException()

View file

@ -1,7 +1,7 @@
ext {
extName = 'NimeGami'
extClass = '.NimeGami'
extVersionCode = 2
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
implementation(project(":lib:synchrony"))
}
}

View file

@ -75,7 +75,11 @@ class NimeGami : ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AniPlay'
extClass = '.AniPlay'
extVersionCode = 10
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View file

@ -84,8 +84,11 @@ class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
.apply { setUrlWithoutDomain(response.request.url.toString()) }
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AniDong'
extClass = '.AniDong'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View file

@ -75,7 +75,11 @@ class AniDong : ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Anime Fire'
extClass = '.AnimeFire'
extVersionCode = 6
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -85,7 +85,11 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -0,0 +1,12 @@
ext {
extName = 'AnimeQ'
extClass = '.AnimeQ'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:blogger-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,260 @@
package eu.kanade.tachiyomi.animeextension.pt.animeq
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.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.bloggerextractor.BloggerExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class AnimeQ : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeQ"
override val baseUrl = "https://animeq.blog"
override val lang = "pt-BR"
override val supportsLatest = true
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
.add("Origin", baseUrl)
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/160679", headers)
override fun popularAnimeSelector() = "div.widget_block:contains(Acessados) a"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = element.selectFirst("a img")!!.attr("title")
thumbnail_url = element.selectFirst("a img")?.tryGetAttr("abs:data-src", "abs:src")
}
override fun popularAnimeNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page", headers)
override fun latestUpdatesSelector() = "div.ContainerEps > article.EpsItem > a"
override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.attr("title")
thumbnail_url = element.selectFirst("div.EpsItemImg > img")?.tryGetAttr("abs:data-src", "abs:src")
}
override fun latestUpdatesNextPageSelector() = "div.ContainerEps a.next.page-numbers"
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = "$baseUrl/page".toHttpUrl().newBuilder()
.addPathSegment(page.toString())
.addQueryParameter("s", query)
.build()
return GET(url, headers = headers)
}
override fun searchAnimeSelector() = "div.ContainerEps > article.AniItem > a"
override fun searchAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.attr("href"))
title = element.attr("title").substringBefore(" Todos os Epis")
thumbnail_url = element.selectFirst("div.AniItemImg > img")?.tryGetAttr("abs:data-src", "abs:src")
}
override fun searchAnimeNextPageSelector() = "div.ContainerEps a.next.page-numbers"
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val doc = getRealDoc(document)
return SAnime.create().apply {
setUrlWithoutDomain(doc.location())
title = doc.title().substringBefore(" Todos os Epis")
thumbnail_url = doc.selectFirst("#capaAnime > img")?.tryGetAttr("abs:data-src", "abs:src")
description = doc.selectFirst("#sinopse2")?.text()
with(doc.selectFirst("div.boxAnimeSobre")!!) {
artist = getInfo("Estúdio")
author = getInfo("Autor") ?: getInfo("Diretor")
genre = getInfo("Tags")
status = parseStatus(getInfo("Episódios"))
}
}
}
// ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> {
return getRealDoc(response.asJsoup())
.select(episodeListSelector())
.map(::episodeFromElement)
.reversed()
}
override fun episodeListSelector() = "#lAnimes a"
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
element.selectFirst("a")!!.attr("title")
.substringBeforeLast(" Final")
.substringAfterLast(" ").let {
name = it.trim()
episode_number = name.toFloatOrNull() ?: 1F
}
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup()
return document.select("div.videoBox div.aba")
.parallelCatchingFlatMapBlocking {
val format = document.selectFirst("a[href=#" + it.attr("id") + "]")?.text()
?: "default"
val quality = when (format) {
"SD" -> "360p"
"HD" -> "720p"
"FHD" -> "1080p"
else -> format
}
val iframeSrc = it.selectFirst("iframe")?.tryGetAttr("data-litespeed-src", "src")
if (!iframeSrc.isNullOrBlank()) {
return@parallelCatchingFlatMapBlocking getVideosFromURL(iframeSrc, quality)
}
it.select("script").mapNotNull {
var javascript = it.attr("src")
?.substringAfter(";base64,")
?.substringBefore('"')
?.let { String(Base64.decode(it, Base64.DEFAULT)) }
if (javascript.isNullOrBlank()) {
javascript = it.data()
}
if (javascript.isNullOrBlank() || "file:" !in javascript) {
return@mapNotNull null
}
val videoUrl = javascript.substringAfter("file:\"").substringBefore('"')
Video(videoUrl, quality, videoUrl)
}
}
}
private val bloggerExtractor by lazy { BloggerExtractor(client) }
private fun getVideosFromURL(url: String, quality: String?): List<Video> {
return when {
"blogger.com" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> emptyList()
}
}
override fun videoListSelector(): String {
throw UnsupportedOperationException()
}
override fun videoFromElement(element: Element): Video {
throw UnsupportedOperationException()
}
override fun videoUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_VALUES
entryValues = PREF_QUALITY_VALUES
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)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ REGEX_QUALITY.find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
// ============================= Utilities ==============================
private fun getRealDoc(document: Document): Document {
val menu = document.selectFirst("div.spr.i-lista")
if (menu != null) {
val originalUrl = menu.parent()!!.attr("href")
val response = client.newCall(GET(originalUrl, headers)).execute()
return response.asJsoup()
}
return document
}
private fun parseStatus(statusString: String?): Int {
return when {
statusString?.trim()?.lowercase() == "em lançamento" -> SAnime.ONGOING
statusString?.trim()?.lowercase() == "em andamento" -> SAnime.ONGOING
statusString?.trim()?.let { REGEX_NUMBER.matches(it) } == true -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
private fun Element.getInfo(key: String): String? {
return selectFirst("div.boxAnimeSobreLinha:has(b:contains($key))")?.run {
text()
.substringAfter(":")
.trim()
.takeUnless { it.isBlank() || it == "???" }
}
}
private fun Element.tryGetAttr(vararg attributeKeys: String): String? {
val attributeKey = attributeKeys.first { hasAttr(it) }
return attributeKey?.let { attr(attributeKey) }
}
companion object {
private val REGEX_QUALITY by lazy { Regex("""(\d+)p""") }
private val REGEX_NUMBER by lazy { Regex("""\d+""") }
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_VALUES = arrayOf("360p", "720p", "1080p")
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animes CX'
extClass = '.AnimesCX'
extVersionCode = 1
extVersionCode = 2
isNsfw = true
}

View file

@ -100,8 +100,11 @@ class AnimesCX : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
.apply { setUrlWithoutDomain(response.request.url.toString()) }
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Animes Digital'
extClass = '.AnimesDigital'
extVersionCode = 3
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"

View file

@ -80,7 +80,11 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,11 +1,11 @@
ext {
extName = 'Animes Games'
extClass = '.AnimesGames'
extVersionCode = 2
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:blogger-extractor'))
}
}

View file

@ -79,7 +79,11 @@ class AnimesGames : ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,9 +1,9 @@
ext {
extName = 'AnimesOnline'
extClass = '.AnimesOnline'
extName = 'Bakashi'
extClass = '.Bakashi'
themePkg = 'dooplay'
baseUrl = 'https://animesonline.nz'
overrideVersionCode = 9
baseUrl = 'https://bakashi.tv'
overrideVersionCode = 10
}
apply from: "$rootDir/common.gradle"
@ -14,4 +14,4 @@ dependencies {
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:mixdrop-extractor"))
implementation(project(":lib:streamtape-extractor"))
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animeextension.pt.animesgratis
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.AnimesOnlinePlayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.NoaExtractor
import eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors.RuplayExtractor
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@ -11,26 +11,24 @@ import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class AnimesOnline : DooPlay(
class Bakashi : DooPlay(
"pt-BR",
"AnimesOnline",
"https://animesonline.nz",
"Bakashi",
"https://bakashi.tv",
) {
override val id: Long = 2969482460524685571L
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div.sidebar.right article > a"
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/")
override fun popularAnimeSelector() = "div.items.featured article div.poster"
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/", headers)
// =============================== Search ===============================
override fun searchAnimeSelector() = "div.result-item article div.thumbnail > a"
@ -64,7 +62,7 @@ class AnimesOnline : DooPlay(
override val prefQualityEntries = prefQualityValues
private val ruplayExtractor by lazy { RuplayExtractor(client) }
private val animesOnlineExtractor by lazy { AnimesOnlinePlayerExtractor(client) }
private val noaExtractor by lazy { NoaExtractor(client, headers) }
private val bloggerExtractor by lazy { BloggerExtractor(client) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
@ -79,45 +77,29 @@ class AnimesOnline : DooPlay(
"streamwish" in name -> streamWishExtractor.videosFromUrl(url)
"filemoon" in name -> filemoonExtractor.videosFromUrl(url)
"mixdrop" in name -> mixDropExtractor.videoFromUrl(url)
"streamtape" in name ->
streamTapeExtractor.videoFromUrl(url)
?.let(::listOf)
?: emptyList()
"/player1/" in url || "/player2/" in url ->
animesOnlineExtractor.videosFromUrl(url)
"streamtape" in name -> streamTapeExtractor.videosFromUrl(url)
"/noance/" in url || "/noa" in url -> noaExtractor.videosFromUrl(url)
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
else -> emptyList()
}
}
private fun getPlayerUrl(player: Element): String? {
val body = FormBody.Builder()
.add("action", "doo_player_ajax")
.add("post", player.attr("data-post"))
.add("nume", player.attr("data-nume"))
.add("type", player.attr("data-type"))
.build()
val playerId = player.attr("data-nume")
val iframe = player.root().selectFirst("div#source-player-$playerId iframe")
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
.execute()
.let { response ->
response.body.string()
.substringAfter("\"embed_url\":\"")
.substringBefore("\",")
.replace("\\", "")
.takeIf(String::isNotBlank)
?.let {
when {
it.contains("$baseUrl/aviso/") ->
it.toHttpUrl().queryParameter("url")
else -> it
}
}
return iframe?.attr("src")?.takeIf(String::isNotBlank)
?.let {
when {
it.contains("/aviso/") ->
it.toHttpUrl().queryParameter("url")
else -> it
}
}
}
// ============================== Filters ===============================
override fun genresListRequest() = GET("$baseUrl/animes/")
override fun genresListRequest() = popularAnimeRequest(0)
override fun genresListSelector() = "div.filter > div.select:first-child option:not([disabled])"
override fun genresListParse(document: Document): Array<Pair<String, String>> {

View file

@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class AnimesOnlinePlayerExtractor(private val client: OkHttpClient) {
class NoaExtractor(private val client: OkHttpClient, private val headers: Headers) {
fun videosFromUrl(url: String): List<Video> {
return client.newCall(GET(url)).execute()
.body.string()
@ -18,7 +19,7 @@ class AnimesOnlinePlayerExtractor(private val client: OkHttpClient) {
.substringAfter(":\"")
.substringBefore('"')
.replace("\\", "")
Video(videoUrl, "Player - $label", videoUrl)
Video(videoUrl, "Player - $label", videoUrl, headers)
}
}
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimesROLL'
extClass = '.AnimesROLL'
extVersionCode = 3
extVersionCode = 4
}
apply from: "$rootDir/common.gradle"

View file

@ -55,7 +55,11 @@ class AnimesROLL : AnimeHttpSource() {
// =============================== Search ===============================
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'AnimesTC'
extClass = '.AnimesTC'
extVersionCode = 6
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

View file

@ -122,7 +122,11 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Better Anime'
extClass = '.BetterAnime'
extVersionCode = 10
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View file

@ -101,7 +101,11 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Donghua no Sekai'
extClass = '.DonghuaNoSekai'
extVersionCode = 2
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

View file

@ -92,7 +92,11 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Doramogo'
extClass = '.Doramogo'
extVersionCode = 1
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View file

@ -71,7 +71,11 @@ class Doramogo : ParsedAnimeHttpSource() {
}
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Flixei'
extClass = '.Flixei'
extVersionCode = 6
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:mixdrop-extractor"))
}
}

View file

@ -105,7 +105,11 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'HentaisTube'
extClass = '.HentaisTube'
extVersionCode = 2
extVersionCode = 3
isNsfw = true
}

View file

@ -111,7 +111,11 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun getFilterList(): AnimeFilterList = HentaisTubeFilters.FILTER_LIST
private fun searchAnimeByIdParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Hinata Soul'
extClass = '.HinataSoul'
extVersionCode = 4
extVersionCode = 5
}
apply from: "$rootDir/common.gradle"

View file

@ -78,7 +78,11 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
val details = animeDetailsParse(response)
val details = animeDetailsParse(response).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -1,7 +1,7 @@
ext {
extName = 'Megaflix'
extClass = '.Megaflix'
extVersionCode = 16
extVersionCode = 17
isNsfw = true
}
@ -13,4 +13,4 @@ dependencies {
implementation(project(":lib:playlist-utils"))
// for mixdrop and megaflix
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}
}

View file

@ -81,7 +81,11 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
val details = animeDetailsParse(response.asJsoup()).apply {
setUrlWithoutDomain(response.request.url.toString())
initialized = true
}
return AnimesPage(listOf(details), false)
}

View file

@ -0,0 +1,18 @@
ext {
extName = 'Pobreflix'
extClass = '.Pobreflix'
themePkg = 'dooplay'
baseUrl = 'https://pobreflix1.art'
overrideVersionCode = 9
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:filemoon-extractor"))
implementation(project(":lib:streamwish-extractor"))
implementation(project(":lib:streamtape-extractor"))
implementation(project(":lib:playlist-utils"))
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.animeextension.pt.pobreflix
import android.util.Base64
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.FireplayerExtractor
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.MyStreamExtractor
import eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors.SuperFlixExtractor
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
class Pobreflix : DooPlay(
"pt-BR",
"Pobreflix",
"https://pobreflix1.art",
) {
// ============================== Popular ===============================
override fun popularAnimeSelector() = "div.featured div.poster"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/series/page/$page/", headers)
// ============================ Video Links =============================
private val embedplayerExtractor by lazy { FireplayerExtractor(client) }
private val brbeastExtractor by lazy { FireplayerExtractor(client, "https://brbeast.com") }
private val superembedsExtractor by lazy { FireplayerExtractor(client, "https://superembeds.com/") }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val mystreamExtractor by lazy { MyStreamExtractor(client, headers) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
private val superflixExtractor by lazy { SuperFlixExtractor(client, headers, ::genericExtractor) }
private val supercdnExtractor by lazy { SuperFlixExtractor(client, headers, ::genericExtractor, "https://supercdn.org") }
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
return doc.select("div.source-box > a").flatMap {
runCatching {
val data = it.attr("href").toHttpUrl().queryParameter("auth")
?.let { Base64.decode(it, Base64.DEFAULT) }
?.let(::String)
?: return@flatMap emptyList()
val url = data.replace("\\", "").substringAfter("url\":\"").substringBefore('"')
genericExtractor(url)
}.getOrElse { emptyList() }
}
}
private fun genericExtractor(url: String, language: String = ""): List<Video> {
val langSubstr = "[$language]"
return when {
url.contains("superflix") ->
superflixExtractor.videosFromUrl(url)
url.contains("supercdn") ->
supercdnExtractor.videosFromUrl(url)
url.contains("filemoon") ->
filemoonExtractor.videosFromUrl(url, "$langSubstr Filemoon - ", headers = headers)
url.contains("watch.brplayer") || url.contains("/watch?v=") ->
mystreamExtractor.videosFromUrl(url, language)
url.contains("brbeast") ->
brbeastExtractor.videosFromUrl(url, language)
url.contains("embedplayer") ->
embedplayerExtractor.videosFromUrl(url, language)
url.contains("superembeds") ->
superembedsExtractor.videosFromUrl(url, language)
url.contains("streamtape") ->
streamtapeExtractor.videosFromUrl(url, "$langSubstr Streamtape")
url.contains("filelions") ->
streamwishExtractor.videosFromUrl(url, videoNameGen = { "$langSubstr FileLions - $it" })
url.contains("streamwish") ->
streamwishExtractor.videosFromUrl(url, videoNameGen = { "$langSubstr Streamwish - $it" })
else -> emptyList()
}
}
}

View file

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
class FireplayerExtractor(private val client: OkHttpClient, private val host: String = "https://embedplayer.online") {
private val headers by lazy {
Headers.headersOf(
"X-Requested-With",
"XMLHttpRequest",
"Referer",
host,
"Origin",
host,
)
}
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, lang: String): List<Video> {
var id = url.substringAfterLast("/")
if (id.length < 32) {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(eval):containsData(p,a,c,k,e,d)")?.data()
?.let(JsUnpacker::unpackAndCombine)
?: doc.selectFirst("script:containsData(FirePlayer)")?.data()
if (script?.contains("FirePlayer(") == true) {
id = script.substringAfter("FirePlayer(\"").substringBefore('"')
}
}
val postUrl = "$host/player/index.php?data=$id&do=getVideo"
val body = FormBody.Builder()
.add("hash", id)
.add("r", "")
.build()
val masterUrl = client.newCall(POST(postUrl, headers, body = body)).execute()
.body.string()
.substringAfter("securedLink\":\"")
.substringBefore('"')
.replace("\\", "")
return playlistUtils.extractFromHls(masterUrl, videoNameGen = { "[$lang] EmbedPlayer - $it" })
}
}

View file

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.animeextension.pt.pobreflix.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
// From animeworldindia
class MyStreamExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, language: String): List<Video> {
val host = url.substringBefore("/watch?")
val response = client.newCall(GET(url, headers)).execute()
val body = response.body.string()
val codePart = body
.substringAfter("sniff(") // Video function
.substringBefore(",[")
val streamCode = codePart
.substringAfterLast(",\"") // our beloved hash
.substringBefore('"')
val id = codePart.substringAfter(",\"").substringBefore('"') // required ID
val streamUrl = "$host/m3u8/$id/$streamCode/master.txt?s=1&cache=1"
val cookie = response.headers.firstOrNull {
it.first.startsWith("set-cookie", true) && it.second.startsWith("PHPSESSID", true)
}?.second?.substringBefore(";") ?: ""
val newHeaders = headers.newBuilder()
.set("cookie", cookie)
.set("accept", "*/*")
.build()
return playlistUtils.extractFromHls(
streamUrl,
masterHeaders = newHeaders,
videoHeaders = newHeaders,
videoNameGen = { "[$language] MyStream: $it" },
)
}
}

Some files were not shown because too many files have changed in this diff Show more