Initial commit

This commit is contained in:
almightyhak 2024-06-20 11:54:12 +07:00
commit 98ed7e8839
2263 changed files with 108711 additions and 0 deletions

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".fr.franime.FrAnimeUrlActivity"
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:scheme="https"
android:host="franime.fr"
android:pathPattern="/anime/..*"
/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,14 @@
ext {
extName = 'FrAnime'
extClass = '.FrAnime'
extVersionCode = 11
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vido-extractor'))
implementation(project(':lib:vk-extractor'))
implementation(project(':lib:sendvid-extractor'))
implementation(project(':lib:sibnet-extractor'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,187 @@
package eu.kanade.tachiyomi.animeextension.fr.franime
import eu.kanade.tachiyomi.animeextension.fr.franime.dto.Anime
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.lib.sendvidextractor.SendvidExtractor
import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor
import eu.kanade.tachiyomi.lib.vkextractor.VkExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class FrAnime : AnimeHttpSource() {
override val name = "FRAnime"
private val domain = "franime.fr"
override val baseUrl = "https://$domain"
private val baseApiUrl = "https://api.$domain/api"
private val baseApiAnimeUrl = "$baseApiUrl/anime"
override val lang = "fr"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Origin", baseUrl)
private val json: Json by injectLazy()
private val database by lazy {
client.newCall(GET("$baseApiUrl/animes/", headers)).execute()
.body.string()
.let { json.decodeFromString<List<Anime>>(it) }
}
// ============================== Popular ===============================
override suspend fun getPopularAnime(page: Int) =
pagesToAnimesPage(database.sortedByDescending { it.note }, page)
override fun popularAnimeParse(response: Response) = throw UnsupportedOperationException()
override fun popularAnimeRequest(page: Int) = throw UnsupportedOperationException()
// =============================== Latest ===============================
override suspend fun getLatestUpdates(page: Int) = pagesToAnimesPage(database.reversed(), page)
override fun latestUpdatesParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
// =============================== Search ===============================
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
val pages = database.filter {
it.title.contains(query, true) ||
it.originalTitle.contains(query, true) ||
it.titlesAlt.en?.contains(query, true) == true ||
it.titlesAlt.enJp?.contains(query, true) == true ||
it.titlesAlt.jaJp?.contains(query, true) == true ||
titleToUrl(it.originalTitle).contains(query)
}
return pagesToAnimesPage(pages, page)
}
override fun searchAnimeParse(response: Response): AnimesPage = throw UnsupportedOperationException()
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException()
// =========================== Anime Details ============================
override suspend fun getAnimeDetails(anime: SAnime): SAnime = anime
override fun animeDetailsParse(response: Response): SAnime = throw UnsupportedOperationException()
// ============================== Episodes ==============================
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val url = (baseUrl + anime.url).toHttpUrl()
val stem = url.encodedPathSegments.last()
val language = url.queryParameter("lang") ?: "vo"
val season = url.queryParameter("s")?.toIntOrNull() ?: 1
val animeData = database.first { titleToUrl(it.originalTitle) == stem }
val episodes = animeData.seasons[season - 1].episodes
.mapIndexedNotNull { index, episode ->
val players = when (language) {
"vo" -> episode.languages.vo
else -> episode.languages.vf
}.players
if (players.isEmpty()) return@mapIndexedNotNull null
SEpisode.create().apply {
setUrlWithoutDomain(anime.url + "&ep=${index + 1}")
name = episode.title
episode_number = (index + 1).toFloat()
}
}
return episodes.sortedByDescending { it.episode_number }
}
override fun episodeListParse(response: Response): List<SEpisode> = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val url = (baseUrl + episode.url).toHttpUrl()
val seasonNumber = url.queryParameter("s")?.toIntOrNull() ?: 1
val episodeNumber = url.queryParameter("ep")?.toIntOrNull() ?: 1
val episodeLang = url.queryParameter("lang") ?: "vo"
val stem = url.encodedPathSegments.last()
val animeData = database.first { titleToUrl(it.originalTitle) == stem }
val episodeData = animeData.seasons[seasonNumber - 1].episodes[episodeNumber - 1]
val videoBaseUrl = "$baseApiAnimeUrl/${animeData.id}/${seasonNumber - 1}/${episodeNumber - 1}"
val players = if (episodeLang == "vo") episodeData.languages.vo.players else episodeData.languages.vf.players
val videos = players.withIndex().parallelCatchingFlatMap { (index, playerName) ->
val apiUrl = "$videoBaseUrl/$episodeLang/$index"
val playerUrl = client.newCall(GET(apiUrl, headers)).await().body.string()
when (playerName) {
"vido" -> listOf(Video(playerUrl, "FRAnime (Vido)", playerUrl))
"sendvid" -> SendvidExtractor(client, headers).videosFromUrl(playerUrl)
"sibnet" -> SibnetExtractor(client).videosFromUrl(playerUrl)
"vk" -> VkExtractor(client, headers).videosFromUrl(playerUrl)
else -> emptyList()
}
}
return videos
}
// ============================= Utilities ==============================
private fun pagesToAnimesPage(pages: List<Anime>, page: Int): AnimesPage {
val chunks = pages.chunked(50)
val hasNextPage = chunks.size > page
val entries = pageToSAnimes(chunks.getOrNull(page - 1) ?: emptyList())
return AnimesPage(entries, hasNextPage)
}
private val titleRegex by lazy { Regex("[^A-Za-z0-9 ]") }
private fun titleToUrl(title: String) = titleRegex.replace(title, "").replace(" ", "-").lowercase()
private fun pageToSAnimes(page: List<Anime>): List<SAnime> {
return page.flatMap { anime ->
anime.seasons.flatMapIndexed { index, season ->
val seasonTitle = anime.title + if (anime.seasons.size > 1) " S${index + 1}" else ""
val hasVostfr = season.episodes.any { ep -> ep.languages.vo.players.isNotEmpty() }
val hasVf = season.episodes.any { ep -> ep.languages.vf.players.isNotEmpty() }
// I want to die for writing this
val languages = listOfNotNull(
if (hasVostfr) Triple("VOSTFR", "vo", hasVf) else null,
if (hasVf) Triple("VF", "vf", hasVostfr) else null,
)
languages.map { lang ->
SAnime.create().apply {
title = seasonTitle + if (lang.third) " (${lang.first})" else ""
thumbnail_url = anime.poster
genre = anime.genres.joinToString()
status = parseStatus(anime.status, anime.seasons.size, index + 1)
description = anime.description
setUrlWithoutDomain("/anime/${titleToUrl(anime.originalTitle)}?lang=${lang.second}&s=${index + 1}")
initialized = true
}
}
}
}
}
private fun parseStatus(statusString: String?, seasonCount: Int = 1, season: Int = 1): Int {
if (season < seasonCount) return SAnime.COMPLETED
return when (statusString?.trim()) {
"EN COURS" -> SAnime.ONGOING
"TERMINÉ" -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
}

View file

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.animeextension.fr.franime
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class FrAnimeUrlActivity : Activity() {
private val tag = "FrAnimeUrlActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size == 2) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", pathSegments[1])
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View file

@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.animeextension.fr.franime.dto
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonUnquotedLiteral
import kotlinx.serialization.json.jsonPrimitive
import java.math.BigInteger
typealias BigIntegerJson =
@Serializable(with = BigIntegerSerializer::class)
BigInteger
@OptIn(ExperimentalSerializationApi::class)
private object BigIntegerSerializer : KSerializer<BigInteger> {
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): BigInteger =
when (decoder) {
is JsonDecoder -> decoder.decodeJsonElement().jsonPrimitive.content.toBigInteger()
else -> decoder.decodeString().toBigInteger()
}
override fun serialize(encoder: Encoder, value: BigInteger) =
when (encoder) {
is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.toString()))
else -> encoder.encodeString(value.toString())
}
}
@Serializable
data class Anime(
@SerialName("themes") val genres: List<String>,
@SerialName("saisons") val seasons: List<Season>,
@SerialName("_id") val uid: String?,
@SerialName("id") val id: BigIntegerJson,
@SerialName("source_url") val sourceUrl: String,
@SerialName("banner") val banner: String?,
@SerialName("affiche") val poster: String,
@SerialName("titleO") val originalTitle: String,
@SerialName("title") val title: String,
@SerialName("titles") val titlesAlt: TitlesAlt,
@SerialName("description") val description: String,
@SerialName("note") val note: Float,
@SerialName("format") val format: String,
@SerialName("startDate") val startDate: String?, // deserialize as date
@SerialName("endDate") val endDate: String?, // ditto
@SerialName("status") val status: String,
@SerialName("nsfw") val nsfw: Boolean,
@SerialName("__v") val uuv: Int?, // no idea wtf is this
@SerialName("affiche_small") val posterSmall: String?,
@SerialName("updatedDate") val updateTime: Long?, // deserialize as timestamp
)
@Serializable
data class Season(
@SerialName("title") val title: String,
@SerialName("episodes") val episodes: List<Episode>,
)
@Serializable
data class Episode(
@SerialName("title") val title: String,
@SerialName("lang") val languages: EpisodeLanguages,
)
@Serializable
data class EpisodeLanguages(
@SerialName("vf") val vf: EpisodeLanguage,
@SerialName("vo") val vo: EpisodeLanguage,
)
@Serializable
data class EpisodeLanguage(
@SerialName("lecteurs") val players: List<String>,
)
@Serializable
data class TitlesAlt(
@SerialName("en") val en: String?,
@SerialName("en_jp") val enJp: String?,
@SerialName("ja_jp") val jaJp: String?,
)