chore: Sync with latest commits
|
@ -3,11 +3,13 @@ package eu.kanade.tachiyomi.lib.vidbomextractor
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class VidBomExtractor(private val client: OkHttpClient) {
|
class VidBomExtractor(private val client: OkHttpClient) {
|
||||||
fun videosFromUrl(url: String): List<Video> {
|
fun videosFromUrl(url: String, headers: Headers? = null): List<Video> {
|
||||||
val doc = client.newCall(GET(url)).execute().asJsoup()
|
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 script = doc.selectFirst("script:containsData(sources)")!!
|
||||||
val data = script.data().substringAfter("sources: [").substringBefore("],")
|
val data = script.data().substringAfter("sources: [").substringBefore("],")
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Sudatchi'
|
extName = 'Sudatchi'
|
||||||
extClass = '.Sudatchi'
|
extClass = '.Sudatchi'
|
||||||
extVersionCode = 1
|
extVersionCode = 3
|
||||||
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -4,12 +4,13 @@ import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
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.DirectoryDto
|
||||||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomeListDto
|
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.EpisodePageDto
|
||||||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.LongAnimeDto
|
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.HomePageDto
|
||||||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.ShortAnimeDto
|
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.SubtitleDto
|
||||||
import eu.kanade.tachiyomi.animeextension.all.sudatchi.dto.WatchDto
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
@ -27,6 +28,7 @@ import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -37,6 +39,8 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val baseUrl = "https://sudatchi.com"
|
override val baseUrl = "https://sudatchi.com"
|
||||||
|
|
||||||
|
private val ipfsUrl = "https://ipfs.animeui.com"
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
@ -52,7 +56,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== 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) {
|
private fun Int.parseStatus() = when (this) {
|
||||||
1 -> SAnime.UNKNOWN // Not Yet Released
|
1 -> SAnime.UNKNOWN // Not Yet Released
|
||||||
|
@ -61,7 +65,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
else -> SAnime.UNKNOWN
|
else -> SAnime.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ShortAnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
|
private fun AnimeDto.toSAnime(titleLang: String) = SAnime.create().apply {
|
||||||
url = "/anime/$slug"
|
url = "/anime/$slug"
|
||||||
title = when (titleLang) {
|
title = when (titleLang) {
|
||||||
"romaji" -> titleRomanji
|
"romaji" -> titleRomanji
|
||||||
|
@ -70,14 +74,19 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
|
} ?: arrayOf(titleEnglish, titleRomanji, titleJapanese, "").firstNotNullOf { it }
|
||||||
description = synopsis
|
description = synopsis
|
||||||
status = statusId.parseStatus()
|
status = statusId.parseStatus()
|
||||||
thumbnail_url = "$baseUrl$imgUrl"
|
thumbnail_url = when {
|
||||||
|
imgUrl.startsWith("/") -> "$baseUrl$imgUrl"
|
||||||
|
else -> "$ipfsUrl/ipfs/$imgUrl"
|
||||||
|
}
|
||||||
genre = animeGenres?.joinToString { it.genre.name }
|
genre = animeGenres?.joinToString { it.genre.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
sudatchiFilters.fetchFilters()
|
sudatchiFilters.fetchFilters()
|
||||||
val titleLang = preferences.title
|
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 ===============================
|
// =============================== Latest ===============================
|
||||||
|
@ -97,7 +106,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||||
val id = query.removePrefix(PREFIX_SEARCH)
|
val id = query.removePrefix(PREFIX_SEARCH)
|
||||||
client.newCall(GET("$baseUrl/api/anime/$id", headers))
|
client.newCall(GET("$baseUrl/anime/$id", headers))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.use(::searchAnimeByIdParse)
|
.use(::searchAnimeByIdParse)
|
||||||
} else {
|
} 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 {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
val url = "$baseUrl/api/directory".toHttpUrl().newBuilder()
|
val url = "$baseUrl/api/directory".toHttpUrl().newBuilder()
|
||||||
|
@ -123,15 +139,18 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
// =========================== Anime Details ============================
|
// =========================== Anime Details ============================
|
||||||
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
|
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
|
||||||
|
|
||||||
override fun animeDetailsRequest(anime: SAnime) = GET("$baseUrl/api${anime.url}", headers)
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
|
val document = response.asJsoup()
|
||||||
override fun animeDetailsParse(response: Response) = response.parseAs<ShortAnimeDto>().toSAnime(preferences.title)
|
val data = document.parseAs<AnimePageDto>().animeData
|
||||||
|
return data.toSAnime(preferences.title)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
|
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
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 {
|
return anime.episodes.map {
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
name = it.title
|
name = it.title
|
||||||
|
@ -148,8 +167,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val jsonString = document.selectFirst("script#__NEXT_DATA__")?.data() ?: return emptyList()
|
val data = document.parseAs<EpisodePageDto>().episodeData
|
||||||
val data = json.decodeFromString<WatchDto>(jsonString).props.pageProps.episodeData
|
|
||||||
val subtitles = json.decodeFromString<List<SubtitleDto>>(data.subtitlesJson)
|
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
|
// 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 :
|
// keeping it in case the simpler solution breaks, can be hardcoded to this for now :
|
||||||
|
@ -158,7 +176,7 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
videoUrl,
|
videoUrl,
|
||||||
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
|
videoNameGen = { "Sudatchi (Private IPFS Gateway) - $it" },
|
||||||
subtitleList = subtitles.map {
|
subtitleList = subtitles.map {
|
||||||
Track("$baseUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
|
Track("$ipfsUrl${it.url}", "${it.subtitlesName.name} (${it.subtitlesName.language})")
|
||||||
}.sort(),
|
}.sort(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -226,6 +244,11 @@ class Sudatchi : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= 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.quality get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||||
private val SharedPreferences.subtitles get() = getString(PREF_SUBTITLES_KEY, PREF_SUBTITLES_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)!!
|
private val SharedPreferences.title get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
|
||||||
|
|
|
@ -4,16 +4,23 @@ import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Genre(val name: String)
|
data class GenreDto(val name: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimeGenreRelation(
|
data class AnimeGenreRelationDto(
|
||||||
@SerialName("Genre")
|
@SerialName("Genre")
|
||||||
val genre: Genre,
|
val genre: GenreDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ShortAnimeDto(
|
data class EpisodeDto(
|
||||||
|
val title: String,
|
||||||
|
val id: Int,
|
||||||
|
val number: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeDto(
|
||||||
val titleRomanji: String?,
|
val titleRomanji: String?,
|
||||||
val titleEnglish: String?,
|
val titleEnglish: String?,
|
||||||
val titleJapanese: String?,
|
val titleJapanese: String?,
|
||||||
|
@ -22,36 +29,46 @@ data class ShortAnimeDto(
|
||||||
val statusId: Int,
|
val statusId: Int,
|
||||||
val imgUrl: String,
|
val imgUrl: String,
|
||||||
@SerialName("AnimeGenres")
|
@SerialName("AnimeGenres")
|
||||||
val animeGenres: List<AnimeGenreRelation>?,
|
val animeGenres: List<AnimeGenreRelationDto>?,
|
||||||
|
@SerialName("Episodes")
|
||||||
|
val episodes: List<EpisodeDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HomeListDto(
|
data class HomePageDto(
|
||||||
@SerialName("AnimeSpotlight")
|
@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
|
@Serializable
|
||||||
data class DirectoryDto(
|
data class DirectoryDto(
|
||||||
val animes: List<ShortAnimeDto>,
|
val animes: List<AnimeDto>,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val pages: 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
|
@Serializable
|
||||||
data class SubtitleLangDto(
|
data class SubtitleLangDto(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@ -65,27 +82,6 @@ data class SubtitleDto(
|
||||||
val subtitlesName: SubtitleLangDto,
|
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
|
@Serializable
|
||||||
data class FilterItemDto(
|
data class FilterItemDto(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'SupJav'
|
extName = 'SupJav'
|
||||||
extClass = '.SupJavFactory'
|
extClass = '.SupJavFactory'
|
||||||
extVersionCode = 8
|
extVersionCode = 9
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,4 +12,4 @@ dependencies {
|
||||||
implementation(project(":lib:streamwish-extractor"))
|
implementation(project(":lib:streamwish-extractor"))
|
||||||
implementation(project(":lib:voe-extractor"))
|
implementation(project(":lib:voe-extractor"))
|
||||||
implementation(project(":lib:playlist-utils"))
|
implementation(project(":lib:playlist-utils"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,11 @@ class SupJav(override val lang: String = "en") : ConfigurableAnimeSource, Parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
src/all/torrentio/AndroidManifest.xml
Normal 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>
|
8
src/all/torrentio/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Torrentio (Torrent / Debrid)'
|
||||||
|
extClass = '.Torrentio'
|
||||||
|
extVersionCode = 1
|
||||||
|
containsNsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/all/torrentio/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/all/torrentio/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
src/all/torrentio/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/all/torrentio/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/all/torrentio/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6 KiB |
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Torrentio Anime (Torrent / Debrid)'
|
extName = 'Torrentio Anime (Torrent / Debrid)'
|
||||||
extClass = '.Torrentio'
|
extClass = '.Torrentio'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
containsNsfw = false
|
containsNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,8 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
val jsonData = response.body.string()
|
val jsonData = response.body.string()
|
||||||
return parseSearchJson(jsonData) }
|
return parseSearchJson(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
@ -258,7 +259,11 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +484,9 @@ class Torrentio : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
if (debridProvider == "none") {
|
if (debridProvider == "none") {
|
||||||
val trackerList = animeTrackers.split(",").map { it.trim() }.filter { it.isNotBlank() }.joinToString("&tr=")
|
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}"
|
"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)
|
Video(urlOrHash, ((stream.name?.replace("Torrentio\n", "") ?: "") + "\n" + stream.title), urlOrHash)
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime4up'
|
extName = 'Anime4up'
|
||||||
extClass = '.Anime4Up'
|
extClass = '.Anime4Up'
|
||||||
extVersionCode = 55
|
extVersionCode = 56
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimeLek'
|
extName = 'AnimeLek'
|
||||||
extClass = '.AnimeLek'
|
extClass = '.AnimeLek'
|
||||||
extVersionCode = 29
|
extVersionCode = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animerco'
|
extName = 'Animerco'
|
||||||
extClass = '.Animerco'
|
extClass = '.Animerco'
|
||||||
extVersionCode = 36
|
extVersionCode = 37
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'asia2tv'
|
extName = 'asia2tv'
|
||||||
extClass = '.Asia2TV'
|
extClass = '.Asia2TV'
|
||||||
extVersionCode = 17
|
extVersionCode = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'MY CIMA'
|
extName = 'MY CIMA'
|
||||||
extClass = '.MyCima'
|
extClass = '.MyCima'
|
||||||
extVersionCode = 20
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib:uqload-extractor'))
|
implementation(project(':lib:uqload-extractor'))
|
||||||
}
|
implementation(project(':lib:vidbom-extractor'))
|
||||||
|
implementation(project(':lib:dood-extractor'))
|
||||||
|
implementation(project(':lib:okru-extractor'))
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.animeextension.ar.mycima
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.animeextension.ar.mycima.extractors.GoVadExtractor
|
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
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.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
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.uqloadextractor.UqloadExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.vidbomextractor.VidBomExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||||
|
@ -23,13 +24,14 @@ import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "MY Cima"
|
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"
|
override val lang = "ar"
|
||||||
|
|
||||||
|
@ -39,13 +41,14 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== popular ==============================
|
// ============================== Popular ==============================
|
||||||
|
override fun popularAnimeSelector(): String =
|
||||||
override fun popularAnimeSelector(): String = "div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
|
"div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
|
||||||
|
|
||||||
override fun popularAnimeNextPageSelector(): String = "ul.page-numbers li a.next"
|
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 {
|
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
|
@ -59,116 +62,125 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
return anime
|
return anime
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== episodes ==============================
|
// ============================== Episodes ==============================
|
||||||
|
|
||||||
override fun episodeListSelector() = "div.Episodes--Seasons--Episodes a"
|
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> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val episodes = mutableListOf<SEpisode>()
|
val document = response.asJsoup()
|
||||||
|
return if (document.select(episodeListSelector()).isNullOrEmpty()) {
|
||||||
var seasonNumber = 1
|
val movieSeries =
|
||||||
fun addEpisodes(document: Document) {
|
document.select("singlerelated.hasdivider:contains(سلسلة) div.Thumb--GridItem a")
|
||||||
if (document.select(episodeListSelector()).isNullOrEmpty()) {
|
if (movieSeries.isNotEmpty()) {
|
||||||
if (!document.select("mycima singlerelated.hasdivider ${popularAnimeSelector()}").isNullOrEmpty()) {
|
movieSeries.sortedByDescending {
|
||||||
document.select("mycima singlerelated.hasdivider ${popularAnimeSelector()}").map { episodes.add(newEpisodeFromElement(it, "mSeries")) }
|
it.selectFirst(".year")!!.text().let(::getNumberFromEpsString)
|
||||||
} else {
|
}.map(::mSeriesEpisode)
|
||||||
episodes.add(newEpisodeFromElement(document.selectFirst("div.Poster--Single-begin > a")!!, "movie"))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
document.select(episodeListSelector()).map { episodes.add(newEpisodeFromElement(it)) }
|
document.selectFirst("div.Poster--Single-begin > a")!!.let(::movieEpisode)
|
||||||
document.selectFirst(seasonsNextPageSelector(seasonNumber))?.let {
|
}
|
||||||
seasonNumber++
|
} else {
|
||||||
addEpisodes(
|
val seasonsList = document.select(seasonsListSelector())
|
||||||
client.newCall(GET(it.attr("abs:href"), headers)).execute().asJsoup(),
|
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 episode = SEpisode.create()
|
||||||
val epNum = getNumberFromEpsString(element.text())
|
episode.setUrlWithoutDomain(
|
||||||
episode.setUrlWithoutDomain(if (type == "mSeries") element.select("a").attr("href") else element.attr("abs:href"))
|
when (type) {
|
||||||
if (type == "series") {
|
"series" -> element.select("a").attr("href")
|
||||||
episode.episode_number = when {
|
else -> element.absUrl("href")
|
||||||
epNum.isNotEmpty() -> epNum.toFloatOrNull() ?: 1F
|
},
|
||||||
else -> 1F
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
episode.name = when (type) {
|
episode.name = when (type) {
|
||||||
"movie" -> "مشاهدة"
|
"series" -> "الموسم $seNum : ${element.text()}"
|
||||||
"mSeries" -> element.select("a").attr("title")
|
"mSeries" -> element.text().replace("مشاهدة فيلم ", "").substringBefore("مترجم")
|
||||||
else -> element.ownerDocument()!!.select("div.List--Seasons--Episodes a.selected").text() + element.text()
|
else -> "مشاهدة"
|
||||||
|
}
|
||||||
|
episode.episode_number = when (type) {
|
||||||
|
"series" -> "$seNum.${element.text().let(::getNumberFromEpsString)}".toFloat()
|
||||||
|
else -> 1F
|
||||||
}
|
}
|
||||||
return episode
|
return episode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
override fun episodeFromElement(element: Element): SEpisode =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
private fun getNumberFromEpsString(epsStr: String): String {
|
private fun getNumberFromEpsString(epsStr: String): String = epsStr.filter { it.isDigit() }
|
||||||
return epsStr.filter { it.isDigit() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== video urls ==============================
|
|
||||||
|
|
||||||
|
// ============================== Video Links ==============================
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
return document.select("ul.WatchServersList li btn").parallelCatchingFlatMapBlocking {
|
return document.select(videoListSelector())
|
||||||
val frameURL = it.attr("data-url")
|
.parallelCatchingFlatMapBlocking(::extractVideos)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
return when {
|
||||||
GOVAD_REGEX.containsMatchIn(url) -> {
|
element.hasClass("MyCimaServer") && "/run/" in iframeUrl -> {
|
||||||
val finalUrl = GOVAD_REGEX.find(url)!!.groupValues[0]
|
val mp4Url = iframeUrl.replace("?Key", "/?Key") + "&auto=true"
|
||||||
val urlHost = GOVAD_REGEX.find(url)!!.groupValues[1]
|
Video(mp4Url, "Default (may take a while)", mp4Url, newHeader).let(::listOf)
|
||||||
GoVadExtractor(client).videosFromUrl("https://www.$finalUrl.html", urlHost)
|
|
||||||
}
|
}
|
||||||
UQLOAD_REGEX.containsMatchIn(url) -> {
|
|
||||||
val finalUrl = UQLOAD_REGEX.find(url)!!.groupValues[0]
|
"govid" in iframeTxt || "vidbom" in iframeTxt || "vidshare" in iframeTxt -> {
|
||||||
UqloadExtractor(client).videosFromUrl("https://www.$finalUrl.html")
|
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
|
else -> null
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoListSelector() = "body"
|
override fun videoListSelector() = "ul.WatchServersList li"
|
||||||
|
|
||||||
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 List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||||
return sortedWith(
|
return sortedWith(
|
||||||
compareBy { it.quality.contains(quality) },
|
compareBy { it.quality.contains(quality) },
|
||||||
).reversed()
|
).reversed()
|
||||||
|
@ -178,65 +190,50 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
// ============================== search ==============================
|
// ============================== Search ==============================
|
||||||
|
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||||
|
|
||||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||||
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 = "ul.page-numbers li a.next"
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = "div.Grid--WecimaPosts div.GridItem div.Thumb--GridItem"
|
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
if (query.isNotBlank()) {
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
val sectionFilter = filterList.find { it is SectionFilter } as SectionFilter
|
||||||
when (filter) {
|
val categoryFilter = filterList.find { it is CategoryFilter } as CategoryFilter
|
||||||
is SearchCategoryList -> {
|
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||||
val catQ = getSearchCategoryList()[filter.state].query
|
val url = baseUrl + if (query.isNotBlank()) {
|
||||||
val catUrl = "$baseUrl/search/$query/" + if (catQ == "page/" && page == 1) "" else "$catQ$page"
|
"/search/$query/${categoryFilter.toUriPart()}$page/"
|
||||||
return GET(catUrl, headers)
|
} else if (sectionFilter.state != 0) {
|
||||||
}
|
"/${sectionFilter.toUriPart()}/page/$page/"
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
"/genre/${genreFilter.toUriPart()}/${categoryFilter.toUriPart()}$page/"
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
return GET(baseUrl, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== details ==============================
|
// ============================== Details ==============================
|
||||||
|
|
||||||
override fun animeDetailsParse(document: Document): SAnime {
|
override fun animeDetailsParse(document: Document): SAnime {
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
anime.title = when {
|
anime.title = when {
|
||||||
document.selectFirst("li:contains(المسلسل) p") != null -> {
|
document.selectFirst("li:contains(المسلسل) p") != null -> {
|
||||||
document.select("li:contains(المسلسل) p").text()
|
document.select("li:contains(المسلسل) p").text()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.selectFirst("singlerelated.hasdivider:contains(سلسلة) a") != null -> {
|
||||||
|
document.selectFirst("singlerelated.hasdivider:contains(سلسلة) a")!!.text()
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
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.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
|
// add alternative name to anime description
|
||||||
document.select("li:contains( بالعربي) > p, li:contains(معروف) > p").text().let {
|
document.select("li:contains( بالعربي) > p, li:contains(معروف) > p").text().let {
|
||||||
if (it.isEmpty().not()) {
|
if (it.isEmpty().not()) {
|
||||||
|
@ -249,101 +246,111 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
return anime
|
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 {
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page", headers)
|
||||||
val anime = SAnime.create()
|
|
||||||
anime.setUrlWithoutDomain(element.select("a").attr("href"))
|
// ============================== Filters ==============================
|
||||||
anime.title = element.select("a > strong").text()
|
override fun getFilterList() = AnimeFilterList(
|
||||||
anime.thumbnail_url = element.select("a > span").attr("data-lazy-style").substringAfter("-image:url(").substringBefore(");")
|
AnimeFilter.Header("هذا القسم يعمل لو كان البحث فارع"),
|
||||||
return anime
|
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")
|
// ============================== Settings ==============================
|
||||||
|
|
||||||
// ============================== 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
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
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 {
|
val videoQualityPref = ListPreference(screen.context).apply {
|
||||||
key = PREF_QUALITY_KEY
|
key = "preferred_quality"
|
||||||
title = PREF_QUALITY_TITLE
|
title = "Preferred quality"
|
||||||
entries = PREF_QUALITY_ENTRIES
|
entries = arrayOf(
|
||||||
entryValues = PREF_QUALITY_ENTRIES.map { it.replace("p", "") }.toTypedArray()
|
"1080p",
|
||||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
"720p",
|
||||||
|
"480p",
|
||||||
|
"360p",
|
||||||
|
"240p",
|
||||||
|
"Vidbom",
|
||||||
|
"Vidshare",
|
||||||
|
"Dood",
|
||||||
|
"Default",
|
||||||
|
)
|
||||||
|
entryValues =
|
||||||
|
arrayOf("1080", "720", "480", "360", "240", "Vidbom", "Vidshare", "Dood", "Default")
|
||||||
|
setDefaultValue("1080")
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
val selected = newValue as String
|
val selected = newValue as String
|
||||||
|
@ -352,26 +359,6 @@ class MyCima : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
preferences.edit().putString(key, entry).commit()
|
preferences.edit().putString(key, entry).commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screen.addPreference(baseUrlPref)
|
|
||||||
screen.addPreference(videoQualityPref)
|
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]+)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Okanime'
|
extName = 'Okanime'
|
||||||
extClass = '.Okanime'
|
extClass = '.Okanime'
|
||||||
extVersionCode = 8
|
extVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -12,4 +12,4 @@ dependencies {
|
||||||
implementation(project(":lib:okru-extractor"))
|
implementation(project(":lib:okru-extractor"))
|
||||||
implementation(project(":lib:vidbom-extractor"))
|
implementation(project(":lib:vidbom-extractor"))
|
||||||
implementation(project(":lib:mp4upload-extractor"))
|
implementation(project(":lib:mp4upload-extractor"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,11 @@ class Okanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Tuktuk Cinema'
|
extName = 'Tuktuk Cinema'
|
||||||
extClass = '.Tuktukcinema'
|
extClass = '.Tuktukcinema'
|
||||||
extVersionCode = 21
|
extVersionCode = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'WIT ANIME'
|
extName = 'WIT ANIME'
|
||||||
extClass = '.WitAnime'
|
extClass = '.WitAnime'
|
||||||
extVersionCode = 48
|
extVersionCode = 49
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Einfach'
|
extName = 'Einfach'
|
||||||
extClass = '.Einfach'
|
extClass = '.Einfach'
|
||||||
extVersionCode = 9
|
extVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -15,4 +15,4 @@ dependencies {
|
||||||
implementation(project(":lib:streamwish-extractor"))
|
implementation(project(":lib:streamwish-extractor"))
|
||||||
implementation(project(":lib:voe-extractor"))
|
implementation(project(":lib:voe-extractor"))
|
||||||
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,11 @@ class Einfach : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'FMovies'
|
extName = 'FMovies'
|
||||||
extClass = '.FMovies'
|
extClass = '.FMovies'
|
||||||
extVersionCode = 24
|
extVersionCode = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -37,7 +37,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "FMovies"
|
override val name = "FMovies"
|
||||||
|
|
||||||
override val baseUrl = "https://fmoviesz.to"
|
override val baseUrl = "https://fmovies24.to"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hstream'
|
extName = 'Hstream'
|
||||||
extClass = '.Hstream'
|
extClass = '.Hstream'
|
||||||
extVersionCode = 8
|
extVersionCode = 9
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,10 @@ class Hstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'KickAssAnime'
|
extName = 'KickAssAnime'
|
||||||
extClass = '.KickAssAnime'
|
extClass = '.KickAssAnime'
|
||||||
extVersionCode = 42
|
extVersionCode = 43
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":lib:cryptoaes"))
|
implementation(project(":lib:cryptoaes"))
|
||||||
implementation(project(":lib:playlist-utils"))
|
implementation(project(":lib:playlist-utils"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,7 +249,11 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'My Running Man'
|
extName = 'My Running Man'
|
||||||
extClass = '.MyRunningMan'
|
extClass = '.MyRunningMan'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -10,4 +10,4 @@ dependencies {
|
||||||
implementation(project(":lib:dood-extractor"))
|
implementation(project(":lib:dood-extractor"))
|
||||||
implementation(project(":lib:mixdrop-extractor"))
|
implementation(project(":lib:mixdrop-extractor"))
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
implementation(project(":lib:streamtape-extractor"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,11 @@ class MyRunningMan : ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime-Sama'
|
extName = 'Anime-Sama'
|
||||||
extClass = '.AnimeSama'
|
extClass = '.AnimeSama'
|
||||||
extVersionCode = 9
|
extVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -64,9 +65,13 @@ class AnimeSama : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
val animes = response.asJsoup()
|
val animes = response.asJsoup()
|
||||||
val seasons = animes.select("h2:contains(derniers ajouts) + .scrollBarStyled > div").flatMap {
|
val seasons = animes.select("#containerAjoutsAnimes > div").flatMap {
|
||||||
val animeUrl = it.getElementsByTag("a").attr("href")
|
val animeUrl = it.getElementsByTag("a").attr("href").toHttpUrl()
|
||||||
fetchAnimeSeasons(animeUrl)
|
val url = animeUrl.newBuilder()
|
||||||
|
.removePathSegment(animeUrl.pathSize - 2)
|
||||||
|
.removePathSegment(animeUrl.pathSize - 3)
|
||||||
|
.build()
|
||||||
|
fetchAnimeSeasons(url.toString())
|
||||||
}
|
}
|
||||||
return AnimesPage(seasons, false)
|
return AnimesPage(seasons, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniSama'
|
extName = 'AniSama'
|
||||||
extClass = '.AniSama'
|
extClass = '.AniSama'
|
||||||
extVersionCode = 3
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -13,4 +13,4 @@ dependencies {
|
||||||
implementation(project(':lib:filemoon-extractor'))
|
implementation(project(':lib:filemoon-extractor'))
|
||||||
implementation(project(':lib:dood-extractor'))
|
implementation(project(':lib:dood-extractor'))
|
||||||
implementation(project(':lib:streamhidevid-extractor'))
|
implementation(project(':lib:streamhidevid-extractor'))
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ class AniSama : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
|
|
||||||
override val name = "AniSama"
|
override val name = "AniSama"
|
||||||
|
|
||||||
override val baseUrl = "https://animesz.xyz"
|
override val baseUrl = "https://v1.anisama.net"
|
||||||
|
|
||||||
override val lang = "fr"
|
override val lang = "fr"
|
||||||
|
|
||||||
|
@ -97,8 +97,11 @@ class AniSama : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||||
val details = animeDetailsParse(response.asJsoup())
|
val details = animeDetailsParse(response.asJsoup()).apply {
|
||||||
.apply { setUrlWithoutDomain(response.request.url.toString()) }
|
setUrlWithoutDomain(response.request.url.toString())
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'NekoSama'
|
extName = 'NekoSama'
|
||||||
extClass = '.NekoSama'
|
extClass = '.NekoSama'
|
||||||
extVersionCode = 11
|
extVersionCode = 10
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,16 +68,18 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val pageBody = response.asJsoup()
|
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 {
|
SEpisode.create().apply {
|
||||||
name = "Episode " + it.text().substringAfter("-").substringBefore("-").trim()
|
name = try { it.episode!! } catch (e: Exception) { "episode" }
|
||||||
setUrlWithoutDomain(it.attr("href"))
|
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()
|
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'NimeGami'
|
extName = 'NimeGami'
|
||||||
extClass = '.NimeGami'
|
extClass = '.NimeGami'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
||||||
implementation(project(":lib:synchrony"))
|
implementation(project(":lib:synchrony"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,11 @@ class NimeGami : ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniPlay'
|
extName = 'AniPlay'
|
||||||
extClass = '.AniPlay'
|
extClass = '.AniPlay'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -84,8 +84,11 @@ class AniPlay : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||||
val details = animeDetailsParse(response)
|
val details = animeDetailsParse(response).apply {
|
||||||
.apply { setUrlWithoutDomain(response.request.url.toString()) }
|
setUrlWithoutDomain(response.request.url.toString())
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AniDong'
|
extName = 'AniDong'
|
||||||
extClass = '.AniDong'
|
extClass = '.AniDong'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -75,7 +75,11 @@ class AniDong : ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime Fire'
|
extName = 'Anime Fire'
|
||||||
extClass = '.AnimeFire'
|
extClass = '.AnimeFire'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -85,7 +85,11 @@ class AnimeFire : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
src/pt/animeq/build.gradle
Normal 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'))
|
||||||
|
}
|
BIN
src/pt/animeq/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/pt/animeq/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/pt/animeq/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
src/pt/animeq/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
src/pt/animeq/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animes CX'
|
extName = 'Animes CX'
|
||||||
extClass = '.AnimesCX'
|
extClass = '.AnimesCX'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,11 @@ class AnimesCX : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
||||||
val details = animeDetailsParse(response.asJsoup())
|
val details = animeDetailsParse(response.asJsoup()).apply {
|
||||||
.apply { setUrlWithoutDomain(response.request.url.toString()) }
|
setUrlWithoutDomain(response.request.url.toString())
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
return AnimesPage(listOf(details), false)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animes Digital'
|
extName = 'Animes Digital'
|
||||||
extClass = '.AnimesDigital'
|
extClass = '.AnimesDigital'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -80,7 +80,11 @@ class AnimesDigital : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Animes Games'
|
extName = 'Animes Games'
|
||||||
extClass = '.AnimesGames'
|
extClass = '.AnimesGames'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib:blogger-extractor'))
|
implementation(project(':lib:blogger-extractor'))
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,11 @@ class AnimesGames : ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimesOnline'
|
extName = 'Bakashi'
|
||||||
extClass = '.AnimesOnline'
|
extClass = '.Bakashi'
|
||||||
themePkg = 'dooplay'
|
themePkg = 'dooplay'
|
||||||
baseUrl = 'https://animesonline.nz'
|
baseUrl = 'https://bakashi.tv'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -14,4 +14,4 @@ dependencies {
|
||||||
implementation(project(":lib:streamwish-extractor"))
|
implementation(project(":lib:streamwish-extractor"))
|
||||||
implementation(project(":lib:mixdrop-extractor"))
|
implementation(project(":lib:mixdrop-extractor"))
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
implementation(project(":lib:streamtape-extractor"))
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.pt.animesgratis
|
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.animeextension.pt.animesgratis.extractors.RuplayExtractor
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
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.lib.streamwishextractor.StreamWishExtractor
|
||||||
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
|
import eu.kanade.tachiyomi.multisrc.dooplay.DooPlay
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
class AnimesOnline : DooPlay(
|
class Bakashi : DooPlay(
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
"AnimesOnline",
|
"Bakashi",
|
||||||
"https://animesonline.nz",
|
"https://bakashi.tv",
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override val id: Long = 2969482460524685571L
|
override val id: Long = 2969482460524685571L
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
override fun popularAnimeSelector() = "div.sidebar.right article > a"
|
override fun popularAnimeSelector() = "div.items.featured article div.poster"
|
||||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/")
|
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/animes/", headers)
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
override fun searchAnimeSelector() = "div.result-item article div.thumbnail > a"
|
override fun searchAnimeSelector() = "div.result-item article div.thumbnail > a"
|
||||||
|
@ -64,7 +62,7 @@ class AnimesOnline : DooPlay(
|
||||||
override val prefQualityEntries = prefQualityValues
|
override val prefQualityEntries = prefQualityValues
|
||||||
|
|
||||||
private val ruplayExtractor by lazy { RuplayExtractor(client) }
|
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 bloggerExtractor by lazy { BloggerExtractor(client) }
|
||||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||||
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
|
||||||
|
@ -79,45 +77,29 @@ class AnimesOnline : DooPlay(
|
||||||
"streamwish" in name -> streamWishExtractor.videosFromUrl(url)
|
"streamwish" in name -> streamWishExtractor.videosFromUrl(url)
|
||||||
"filemoon" in name -> filemoonExtractor.videosFromUrl(url)
|
"filemoon" in name -> filemoonExtractor.videosFromUrl(url)
|
||||||
"mixdrop" in name -> mixDropExtractor.videoFromUrl(url)
|
"mixdrop" in name -> mixDropExtractor.videoFromUrl(url)
|
||||||
"streamtape" in name ->
|
"streamtape" in name -> streamTapeExtractor.videosFromUrl(url)
|
||||||
streamTapeExtractor.videoFromUrl(url)
|
"/noance/" in url || "/noa" in url -> noaExtractor.videosFromUrl(url)
|
||||||
?.let(::listOf)
|
|
||||||
?: emptyList()
|
|
||||||
"/player1/" in url || "/player2/" in url ->
|
|
||||||
animesOnlineExtractor.videosFromUrl(url)
|
|
||||||
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
|
"/player/" in url -> bloggerExtractor.videosFromUrl(url, headers)
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlayerUrl(player: Element): String? {
|
private fun getPlayerUrl(player: Element): String? {
|
||||||
val body = FormBody.Builder()
|
val playerId = player.attr("data-nume")
|
||||||
.add("action", "doo_player_ajax")
|
val iframe = player.root().selectFirst("div#source-player-$playerId iframe")
|
||||||
.add("post", player.attr("data-post"))
|
|
||||||
.add("nume", player.attr("data-nume"))
|
|
||||||
.add("type", player.attr("data-type"))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", headers, body))
|
return iframe?.attr("src")?.takeIf(String::isNotBlank)
|
||||||
.execute()
|
?.let {
|
||||||
.let { response ->
|
when {
|
||||||
response.body.string()
|
it.contains("/aviso/") ->
|
||||||
.substringAfter("\"embed_url\":\"")
|
it.toHttpUrl().queryParameter("url")
|
||||||
.substringBefore("\",")
|
else -> it
|
||||||
.replace("\\", "")
|
}
|
||||||
.takeIf(String::isNotBlank)
|
|
||||||
?.let {
|
|
||||||
when {
|
|
||||||
it.contains("$baseUrl/aviso/") ->
|
|
||||||
it.toHttpUrl().queryParameter("url")
|
|
||||||
else -> it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Filters ===============================
|
// ============================== 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 genresListSelector() = "div.filter > div.select:first-child option:not([disabled])"
|
||||||
|
|
||||||
override fun genresListParse(document: Document): Array<Pair<String, String>> {
|
override fun genresListParse(document: Document): Array<Pair<String, String>> {
|
|
@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.animeextension.pt.animesgratis.extractors
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
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> {
|
fun videosFromUrl(url: String): List<Video> {
|
||||||
return client.newCall(GET(url)).execute()
|
return client.newCall(GET(url)).execute()
|
||||||
.body.string()
|
.body.string()
|
||||||
|
@ -18,7 +19,7 @@ class AnimesOnlinePlayerExtractor(private val client: OkHttpClient) {
|
||||||
.substringAfter(":\"")
|
.substringAfter(":\"")
|
||||||
.substringBefore('"')
|
.substringBefore('"')
|
||||||
.replace("\\", "")
|
.replace("\\", "")
|
||||||
Video(videoUrl, "Player - $label", videoUrl)
|
Video(videoUrl, "Player - $label", videoUrl, headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimesROLL'
|
extName = 'AnimesROLL'
|
||||||
extClass = '.AnimesROLL'
|
extClass = '.AnimesROLL'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -55,7 +55,11 @@ class AnimesROLL : AnimeHttpSource() {
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'AnimesTC'
|
extName = 'AnimesTC'
|
||||||
extClass = '.AnimesTC'
|
extClass = '.AnimesTC'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -122,7 +122,11 @@ class AnimesTC : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Better Anime'
|
extName = 'Better Anime'
|
||||||
extClass = '.BetterAnime'
|
extClass = '.BetterAnime'
|
||||||
extVersionCode = 10
|
extVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -101,7 +101,11 @@ class BetterAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Donghua no Sekai'
|
extName = 'Donghua no Sekai'
|
||||||
extClass = '.DonghuaNoSekai'
|
extClass = '.DonghuaNoSekai'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -92,7 +92,11 @@ class DonghuaNoSekai : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Doramogo'
|
extName = 'Doramogo'
|
||||||
extClass = '.Doramogo'
|
extClass = '.Doramogo'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -71,7 +71,11 @@ class Doramogo : ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Flixei'
|
extName = 'Flixei'
|
||||||
extClass = '.Flixei'
|
extClass = '.Flixei'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -9,4 +9,4 @@ apply from: "$rootDir/common.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":lib:streamtape-extractor"))
|
implementation(project(":lib:streamtape-extractor"))
|
||||||
implementation(project(":lib:mixdrop-extractor"))
|
implementation(project(":lib:mixdrop-extractor"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,11 @@ class Flixei : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'HentaisTube'
|
extName = 'HentaisTube'
|
||||||
extClass = '.HentaisTube'
|
extClass = '.HentaisTube'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,11 @@ class HentaisTube : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override fun getFilterList(): AnimeFilterList = HentaisTubeFilters.FILTER_LIST
|
override fun getFilterList(): AnimeFilterList = HentaisTubeFilters.FILTER_LIST
|
||||||
|
|
||||||
private fun searchAnimeByIdParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Hinata Soul'
|
extName = 'Hinata Soul'
|
||||||
extClass = '.HinataSoul'
|
extClass = '.HinataSoul'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -78,7 +78,11 @@ class HinataSoul : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Megaflix'
|
extName = 'Megaflix'
|
||||||
extClass = '.Megaflix'
|
extClass = '.Megaflix'
|
||||||
extVersionCode = 16
|
extVersionCode = 17
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,4 +13,4 @@ dependencies {
|
||||||
implementation(project(":lib:playlist-utils"))
|
implementation(project(":lib:playlist-utils"))
|
||||||
// for mixdrop and megaflix
|
// for mixdrop and megaflix
|
||||||
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,11 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchAnimeByPathParse(response: Response): AnimesPage {
|
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)
|
return AnimesPage(listOf(details), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
src/pt/pobreflix/build.gradle
Normal 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"
|
||||||
|
}
|
BIN
src/pt/pobreflix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/pt/pobreflix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/pt/pobreflix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
src/pt/pobreflix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/pt/pobreflix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" })
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|