AnimeSama: Switch to New Endpoint and Direct AnimeSama Link Integration (#10802)

* Open URL in apps + new way to get all chapters

* AnimeSama: Update extVersionCode

* AnimeSama: Changes requested by vetleledaal

* AnimeSama: Fix special chapters that could not be opened
This commit is contained in:
CriosChan 2025-10-04 00:12:26 +02:00 committed by Draff
parent 3b0eb9a789
commit 0c8a27b820
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 142 additions and 50 deletions

View File

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

View File

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

View File

@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.extension.fr.animesama
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@ -12,6 +14,8 @@ import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -20,6 +24,7 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class AnimeSama : ParsedHttpSource() {
@ -96,7 +101,7 @@ class AnimeSama : ParsedHttpSource() {
return SManga.create().apply {
title = element.select("h1").text()
setUrlWithoutDomain(element.select("a").attr("href"))
thumbnail_url = element.select("img").attr("src")
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
}
@ -113,7 +118,7 @@ class AnimeSama : ParsedHttpSource() {
return SManga.create().apply {
title = element.select("h1").text()
setUrlWithoutDomain(element.select("a").attr("href").removeSuffix("scan/vf/"))
thumbnail_url = element.select("img").attr("src")
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
}
@ -135,6 +140,51 @@ class AnimeSama : ParsedHttpSource() {
return GET(url, headers)
}
private fun detailsParse(response: Response): SManga? {
val document = response.asJsoup()
val scriptContent = document.select("script:containsData(panneauScan(\"nom\", \"url\"))").toString()
val splitedContent = scriptContent.split(";").toMutableList()
// Remove exemple
splitedContent.removeAt(0)
val pattern = """panneauScan\("(.+?)", "(.+?)"\)""".toRegex()
val numberOfScans = splitedContent.count { line ->
val matchResult = pattern.find(line)
matchResult != null && !matchResult.groupValues[2].contains("va")
}
if (numberOfScans == 0) return null
val manga: SManga = SManga.create()
manga.description = document.select("#sousBlocMiddle > div h2:contains(Synopsis)+p").text()
manga.genre = document.select("#sousBlocMiddle > div h2:contains(Genres)+a").text()
manga.title = document.select("#titreOeuvre").text()
manga.thumbnail_url = document.selectFirst("#coverOeuvre")?.absUrl("src")
manga.setUrlWithoutDomain(document.baseUri())
return manga
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith("ID:")) {
val id = query.substringAfterLast("ID:")
val mangaUrl = "$baseUrl/catalogue".toHttpUrl()
.newBuilder()
.addPathSegment(id)
.build()
val requestToCheckManga = GET(mangaUrl, headers)
client.newCall(requestToCheckManga).asObservableSuccess().map {
MangasPage(
listOfNotNull(detailsParse(it)),
false,
)
}
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
@ -144,7 +194,7 @@ class AnimeSama : ParsedHttpSource() {
description = document.select("#sousBlocMiddle > div h2:contains(Synopsis)+p").text()
genre = document.select("#sousBlocMiddle > div h2:contains(Genres)+a").text()
title = document.select("#titreOeuvre").text()
thumbnail_url = document.select("#coverOeuvre").attr("src")
thumbnail_url = document.selectFirst("#coverOeuvre")?.absUrl("src")
}
// Chapters
@ -162,22 +212,16 @@ class AnimeSama : ParsedHttpSource() {
private fun parseChapterFromResponse(response: Response, translationName: String): List<SChapter> {
val document = response.asJsoup()
val chapterUrl = document.baseUri().toHttpUrl()
val title = document.select("#titreOeuvre").text()
val chapterUrl = "$baseUrl/s2/scans/get_nb_chap_et_img.php".toHttpUrl()
.newBuilder()
.query(null)
.addPathSegment("episodes.js")
.addQueryParameter("title", document.select("#titreOeuvre").text())
.addQueryParameter("oeuvre", title)
.build()
val requestToFetchChapters = GET(chapterUrl, headers)
val javascriptFile = client.newCall(requestToFetchChapters).execute()
val javascriptFileContent = javascriptFile.body.string()
val parsedJavascriptFileToJson = javascriptFileContent
.let { Regex("""eps(\d+)""").findAll(it) }
.map { it.groupValues[1].toInt() }
.distinct() // Remove duplicate episodes
.sortedDescending().toList()
val apiNbChapImgResponse = client.newCall(requestToFetchChapters).execute()
val apiNbChapImgJson = Json.decodeFromString<Map<String, Int>>(apiNbChapImgResponse.body.string())
val parsedChapterList: MutableList<SChapter> = ArrayList()
var chapterDelay = 0
@ -197,18 +241,18 @@ class AnimeSama : ParsedHttpSource() {
parsedChapterList.add(
SChapter.create().apply {
name = "Chapitre $i"
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).build().toString())
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).addQueryParameter("title", title).build().toString())
scanlator = translationName
},
)
}
}
specialRegex.find(command) != null -> {
val title = specialRegex.find(command)!!.groupValues[1]
val chapterTitle = specialRegex.find(command)!!.groupValues[1]
parsedChapterList.add(
SChapter.create().apply {
name = "Chapitre $title"
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).build().toString())
name = "Chapitre $chapterTitle"
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).addQueryParameter("title", title).build().toString())
scanlator = translationName
},
)
@ -218,11 +262,11 @@ class AnimeSama : ParsedHttpSource() {
}
}
}
for (index in parsedChapterList.size until parsedJavascriptFileToJson.size) {
(parsedChapterList.size until apiNbChapImgJson.size).forEach { index ->
parsedChapterList.add(
SChapter.create().apply {
name = "Chapitre " + (parsedChapterList.size + 1 - chapterDelay)
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).build().toString())
setUrlWithoutDomain(chapterUrl.newBuilder().addQueryParameter("id", (parsedChapterList.size + 1).toString()).addQueryParameter("title", title).build().toString())
scanlator = translationName
},
)
@ -239,14 +283,14 @@ class AnimeSama : ParsedHttpSource() {
splitedContent.removeAt(0)
val parsedChapterList: MutableList<SChapter> = mutableListOf()
val pattern = """panneauScan\("(.+?)", "(.+?)"\)""".toRegex()
val scanPattern = Regex("""(Scans|\(|\))""")
splitedContent.forEach { line ->
val pattern = """panneauScan\("(.+?)", "(.+?)"\)""".toRegex()
val matchResult = pattern.find(line)
if (matchResult != null) {
val (scanTitle, scanUrl) = matchResult.destructured
if (!scanUrl.contains("va")) {
val scanlatorGroup = scanTitle.replace(Regex("""(Scans|\(|\))"""), "").trim()
val scanlatorGroup = scanTitle.replace(scanPattern, "").trim()
val fetchExistentSubMangas = GET(
url.newBuilder()
.addPathSegments(
@ -260,7 +304,7 @@ class AnimeSama : ParsedHttpSource() {
}
}
parsedChapterList.sortBy { chapter -> ("$baseUrl${chapter.url}").toHttpUrl().queryParameter("id")?.toIntOrNull() }
parsedChapterList.sortBy { chapter -> "$baseUrl${chapter.url}".toHttpUrl().queryParameter("id")?.toIntOrNull() }
return parsedChapterList.asReversed()
}
@ -272,32 +316,19 @@ class AnimeSama : ParsedHttpSource() {
val title = url.queryParameter("title")
val chapter = url.queryParameter("id")
val documentString = document.body().toString()
val chapterUrl = "$baseUrl/s2/scans/get_nb_chap_et_img.php".toHttpUrl()
.newBuilder()
.addQueryParameter("oeuvre", title)
.build()
val requestToFetchChapters = GET(chapterUrl, headers)
val allChapters: Map<Int, Int> = Regex("""eps(\d+)\s*(?:=\s*\[(.*?)]|\.length\s*=\s*(\d+))""")
.findAll(documentString)
.associate { match ->
val episode = match.groupValues[1].toInt()
val arrayContent = match.groupValues[2]
val explicitLength = match.groupValues[3]
val apiNbChapImgResponse = client.newCall(requestToFetchChapters).execute()
val apiNbChapImgJson = Json.decodeFromString<Map<String, Int>>(apiNbChapImgResponse.body.string())
val imageCount = apiNbChapImgJson[chapter] ?: 0
val length = when {
explicitLength.isNotEmpty() -> explicitLength.toInt()
arrayContent.isNotEmpty() -> arrayContent.split(Regex(",\\s*")).count { it.isNotBlank() }
else -> 0
}
episode to length
}
val chapterSize = allChapters[chapter?.toInt()] ?: 1
val imageList = mutableListOf<Page>()
for (index in 1 until chapterSize + 1) {
imageList.add(
Page(index, imageUrl = "$cdn$title/$chapter/$index.jpg"),
)
}
val imageList = (1..imageCount).map { index ->
Page(index, imageUrl = "$cdn$title/$chapter/$index.jpg")
}.toMutableList()
return imageList
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.extension.fr.animesama
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://anime-sama.fr/catalogue/xxxxxx/ intents and redirects them to
* the main Tachiyomi process.
*/
class AnimeSamaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "ID:$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("AnimeSamaUrlActivity", e.toString())
}
} else {
Log.e("AnimeSamaUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}