Fix neko sama + FRAnime #128
8 changed files with 100 additions and 33 deletions
7
lib/vidmoly-extractor/build.gradle.kts
Normal file
7
lib/vidmoly-extractor/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:playlist-utils"))
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.vidmolyextractor
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.internal.EMPTY_HEADERS
|
||||||
|
|
||||||
|
class VidMolyExtractor(private val client: OkHttpClient, headers: Headers = EMPTY_HEADERS) {
|
||||||
|
|
||||||
|
private val baseUrl = "https://vidmoly.to"
|
||||||
|
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||||
|
|
||||||
|
private val headers: Headers = headers.newBuilder()
|
||||||
|
.set("Origin", baseUrl)
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val sourcesRegex = Regex("sources: (.*?]),")
|
||||||
|
private val urlsRegex = Regex("""file:"(.*?)"""")
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||||
|
val document = client.newCall(
|
||||||
|
GET(url, headers.newBuilder().set("Sec-Fetch-Dest", "iframe").build())
|
||||||
|
).execute().asJsoup()
|
||||||
|
val script = document.selectFirst("script:containsData(sources)")!!.data()
|
||||||
|
val sources = sourcesRegex.find(script)!!.groupValues[1]
|
||||||
|
val urls = urlsRegex.findAll(sources).map { it.groupValues[1] }.toList()
|
||||||
|
return urls.flatMap {
|
||||||
|
playlistUtils.extractFromHls(it,
|
||||||
|
videoNameGen = { quality -> "${prefix}VidMoly - $quality" },
|
||||||
|
masterHeaders = headers,
|
||||||
|
videoHeaders = headers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,20 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".fr.franime.FrAnimeUrlActivity"
|
android:name=".fr.franime.FrAnimeUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
>
|
>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data
|
||||||
android:scheme="https"
|
android:scheme="https"
|
||||||
android:host="franime.fr"
|
android:host="franime.fr"
|
||||||
android:pathPattern="/anime/..*"
|
android:pathPattern="/anime/..*"
|
||||||
/>
|
/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -11,4 +11,5 @@ dependencies {
|
||||||
implementation(project(':lib:vk-extractor'))
|
implementation(project(':lib:vk-extractor'))
|
||||||
implementation(project(':lib:sendvid-extractor'))
|
implementation(project(':lib:sendvid-extractor'))
|
||||||
implementation(project(':lib:sibnet-extractor'))
|
implementation(project(':lib:sibnet-extractor'))
|
||||||
|
implementation project(':lib:vidmoly-extractor')
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
|
import eu.kanade.tachiyomi.lib.sendvidextractor.SendvidExtractor
|
||||||
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
|
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.vidmolyextractor.VidMolyExtractor
|
||||||
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
|
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
@ -101,7 +102,7 @@ class FrAnime : AnimeHttpSource() {
|
||||||
|
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
|
setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
|
||||||
name = episode.title
|
name = episode.title ?: "Episode ${index + 1}"
|
||||||
episode_number = (index + 1).toFloat()
|
episode_number = (index + 1).toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,14 +124,19 @@ class FrAnime : AnimeHttpSource() {
|
||||||
|
|
||||||
val players = if (episodeLang == "vo") episodeData.languages.vo.players else episodeData.languages.vf.players
|
val players = if (episodeLang == "vo") episodeData.languages.vo.players else episodeData.languages.vf.players
|
||||||
|
|
||||||
|
val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
|
||||||
|
val sibnetExtractor by lazy { SibnetExtractor(client) }
|
||||||
|
val vkExtractor by lazy { VkExtractor(client, headers) }
|
||||||
|
val vidMolyExtractor by lazy { VidMolyExtractor(client) }
|
||||||
|
|
||||||
val videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
|
val videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
|
||||||
val apiUrl = "$videoBaseUrl/$episodeLang/$index"
|
val apiUrl = "$videoBaseUrl/$episodeLang/$index"
|
||||||
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
|
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
|
||||||
when (playerName) {
|
when (playerName) {
|
||||||
"vido" -> listOf(Video(playerUrl, "FRAnime (Vido)", playerUrl))
|
"sendvid" -> sendvidExtractor.videosFromUrl(playerUrl)
|
||||||
"sendvid" -> SendvidExtractor(client, headers).videosFromUrl(playerUrl)
|
"sibnet" -> sibnetExtractor.videosFromUrl(playerUrl)
|
||||||
"sibnet" -> SibnetExtractor(client).videosFromUrl(playerUrl)
|
"vk" -> vkExtractor.videosFromUrl(playerUrl)
|
||||||
"vk" -> VkExtractor(client, headers).videosFromUrl(playerUrl)
|
"vidmoly" -> vidMolyExtractor.videosFromUrl(playerUrl)
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ typealias BigIntegerJson =
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
private object BigIntegerSerializer : KSerializer<BigInteger> {
|
private object BigIntegerSerializer : KSerializer<BigInteger> {
|
||||||
|
|
||||||
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
|
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): BigInteger =
|
override fun deserialize(decoder: Decoder): BigInteger =
|
||||||
|
@ -68,7 +67,7 @@ data class Season(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Episode(
|
data class Episode(
|
||||||
@SerialName("title") val title: String = "!No Title!",
|
@SerialName("title") val title: String?,
|
||||||
@SerialName("lang") val languages: EpisodeLanguages,
|
@SerialName("lang") val languages: EpisodeLanguages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'NekoSama'
|
extName = 'NekoSama'
|
||||||
extClass = '.NekoSama'
|
extClass = '.NekoSama'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.parseAs
|
import eu.kanade.tachiyomi.util.parseAs
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
@ -28,7 +29,14 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
|
|
||||||
override val name = "Neko-Sama"
|
override val name = "Neko-Sama"
|
||||||
|
|
||||||
override val baseUrl by lazy { "https://" + preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
|
override val baseUrl by lazy {
|
||||||
|
val domain = preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||||
|
HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host(domain)
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
override val lang = "fr"
|
override val lang = "fr"
|
||||||
|
|
||||||
|
@ -72,17 +80,19 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
else -> "vostfr"
|
else -> "vostfr"
|
||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
val url = when {
|
||||||
query.isNotBlank() -> GET("$baseUrl/animes-search-$typeSearch.json?$query")
|
query.isNotBlank() -> "$baseUrl/animes-search-$typeSearch.json?$query"
|
||||||
typeFilter.state != 0 || query.isNotBlank() -> when (page) {
|
typeFilter.state != 0 || query.isNotBlank() -> when (page) {
|
||||||
1 -> GET("$baseUrl/${typeFilter.toUriPart()}")
|
1 -> "$baseUrl/${typeFilter.toUriPart()}"
|
||||||
else -> GET("$baseUrl/${typeFilter.toUriPart()}/$page")
|
else -> "$baseUrl/${typeFilter.toUriPart()}/page$page"
|
||||||
}
|
}
|
||||||
else -> when (page) {
|
else -> when (page) {
|
||||||
1 -> GET("$baseUrl/anime/")
|
1 -> "$baseUrl/anime/"
|
||||||
else -> GET("$baseUrl/anime/page/$page")
|
else -> "$baseUrl/anime/page$page"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return GET(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
@ -95,11 +105,13 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
val animes = jsonSearch
|
val animes = jsonSearch
|
||||||
.filter { it.title.orEmpty().lowercase().contains(query) }
|
.filter { it.title.orEmpty().lowercase().contains(query) }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
SAnime.create().apply {
|
val anime = SAnime.create().apply {
|
||||||
url = it.url ?: return@mapNotNull null
|
url = it.url?.substringAfterLast("/")?.substringBefore("-") ?: return@mapNotNull null
|
||||||
title = it.title ?: return@mapNotNull null
|
title = it.title ?: return@mapNotNull null
|
||||||
thumbnail_url = it.url_image ?: "$baseUrl/images/default_poster.png"
|
thumbnail_url = it.url_image ?: "$baseUrl/images/default_poster.png"
|
||||||
|
setUrlWithoutDomain(url) // call setUrlWithoutDomain on the SAnime instance
|
||||||
}
|
}
|
||||||
|
anime
|
||||||
}
|
}
|
||||||
AnimesPage(animes, false)
|
AnimesPage(animes, false)
|
||||||
}
|
}
|
||||||
|
@ -131,9 +143,10 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
SAnime.create().apply {
|
SAnime.create().apply {
|
||||||
val itemUrl = item.url ?: return@mapNotNull null
|
val itemUrl = item.url ?: return@mapNotNull null
|
||||||
title = item.title ?: return@mapNotNull null
|
title = item.title ?: return@mapNotNull null
|
||||||
val type = itemUrl.substringAfterLast("-")
|
val animeId = itemUrl.substringAfterLast("/").substringBefore("-")
|
||||||
url = itemUrl.replace("episode", "info").substringBeforeLast("-").substringBeforeLast("-") + "-$type"
|
val titleSlug = title.replace("[^a-zA-Z0-9 -]".toRegex(), "").replace(" ", "-").lowercase()
|
||||||
thumbnail_url = item.url_image ?: "$baseUrl/images/default_poster.png"
|
url = "anime/info/$animeId-${titleSlug}_vostfr"
|
||||||
|
thumbnail_url = item.url_image ?: "/images/default_poster.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,8 +192,9 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
override fun episodeFromElement(element: Element) = SEpisode.create().apply {
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
val text = element.text()
|
val text = element.text()
|
||||||
name = text.substringBeforeLast(" - ")
|
val episodeNumber = text.substringAfterLast("- ").toFloatOrNull() ?: 0F
|
||||||
episode_number = text.substringAfterLast("- ").toFloatOrNull() ?: 0F
|
name = "Épisode ${episodeNumber.toInt()}"
|
||||||
|
episode_number = episodeNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ Video Links =============================
|
// ============================ Video Links =============================
|
||||||
|
@ -291,8 +305,8 @@ class NekoSama : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||||
private val PLAYERS_REGEX = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
|
private val PLAYERS_REGEX = Regex("video\\s*\\[\\d*]\\s*=\\s*'(.*?)'")
|
||||||
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
||||||
private const val PREF_DOMAIN_TITLE = "Preferred domain"
|
private const val PREF_DOMAIN_TITLE = "Preferred domain"
|
||||||
private const val PREF_DOMAIN_DEFAULT = "animecat.net"
|
|
||||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animecat.net", "neko-sama.fr")
|
private val PREF_DOMAIN_ENTRIES = arrayOf("animecat.net", "neko-sama.fr")
|
||||||
|
private const val PREF_DOMAIN_DEFAULT = "animecat.net"
|
||||||
|
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue