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 { ext {
extName = 'AnimeSama' extName = 'AnimeSama'
extClass = '.AnimeSama' extClass = '.AnimeSama'
extVersionCode = 7 extVersionCode = 8
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.extension.fr.animesama
import android.util.Log import android.util.Log
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -20,6 +24,7 @@ import okhttp3.Request
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
import rx.Observable
class AnimeSama : ParsedHttpSource() { class AnimeSama : ParsedHttpSource() {
@ -96,7 +101,7 @@ class AnimeSama : ParsedHttpSource() {
return SManga.create().apply { return SManga.create().apply {
title = element.select("h1").text() title = element.select("h1").text()
setUrlWithoutDomain(element.select("a").attr("href")) 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 { return SManga.create().apply {
title = element.select("h1").text() title = element.select("h1").text()
setUrlWithoutDomain(element.select("a").attr("href").removeSuffix("scan/vf/")) 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) 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 searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) 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() description = document.select("#sousBlocMiddle > div h2:contains(Synopsis)+p").text()
genre = document.select("#sousBlocMiddle > div h2:contains(Genres)+a").text() genre = document.select("#sousBlocMiddle > div h2:contains(Genres)+a").text()
title = document.select("#titreOeuvre").text() title = document.select("#titreOeuvre").text()
thumbnail_url = document.select("#coverOeuvre").attr("src") thumbnail_url = document.selectFirst("#coverOeuvre")?.absUrl("src")
} }
// Chapters // Chapters
@ -162,22 +212,16 @@ class AnimeSama : ParsedHttpSource() {
private fun parseChapterFromResponse(response: Response, translationName: String): List<SChapter> { private fun parseChapterFromResponse(response: Response, translationName: String): List<SChapter> {
val document = response.asJsoup() 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() .newBuilder()
.query(null) .addQueryParameter("oeuvre", title)
.addPathSegment("episodes.js")
.addQueryParameter("title", document.select("#titreOeuvre").text())
.build() .build()
val requestToFetchChapters = GET(chapterUrl, headers) val requestToFetchChapters = GET(chapterUrl, headers)
val javascriptFile = client.newCall(requestToFetchChapters).execute()
val javascriptFileContent = javascriptFile.body.string()
val parsedJavascriptFileToJson = javascriptFileContent val apiNbChapImgResponse = client.newCall(requestToFetchChapters).execute()
.let { Regex("""eps(\d+)""").findAll(it) } val apiNbChapImgJson = Json.decodeFromString<Map<String, Int>>(apiNbChapImgResponse.body.string())
.map { it.groupValues[1].toInt() }
.distinct() // Remove duplicate episodes
.sortedDescending().toList()
val parsedChapterList: MutableList<SChapter> = ArrayList() val parsedChapterList: MutableList<SChapter> = ArrayList()
var chapterDelay = 0 var chapterDelay = 0
@ -197,18 +241,18 @@ class AnimeSama : ParsedHttpSource() {
parsedChapterList.add( parsedChapterList.add(
SChapter.create().apply { SChapter.create().apply {
name = "Chapitre $i" 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 scanlator = translationName
}, },
) )
} }
} }
specialRegex.find(command) != null -> { specialRegex.find(command) != null -> {
val title = specialRegex.find(command)!!.groupValues[1] val chapterTitle = specialRegex.find(command)!!.groupValues[1]
parsedChapterList.add( parsedChapterList.add(
SChapter.create().apply { SChapter.create().apply {
name = "Chapitre $title" name = "Chapitre $chapterTitle"
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 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( parsedChapterList.add(
SChapter.create().apply { SChapter.create().apply {
name = "Chapitre " + (parsedChapterList.size + 1 - chapterDelay) 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 scanlator = translationName
}, },
) )
@ -239,14 +283,14 @@ class AnimeSama : ParsedHttpSource() {
splitedContent.removeAt(0) splitedContent.removeAt(0)
val parsedChapterList: MutableList<SChapter> = mutableListOf() val parsedChapterList: MutableList<SChapter> = mutableListOf()
val pattern = """panneauScan\("(.+?)", "(.+?)"\)""".toRegex()
val scanPattern = Regex("""(Scans|\(|\))""")
splitedContent.forEach { line -> splitedContent.forEach { line ->
val pattern = """panneauScan\("(.+?)", "(.+?)"\)""".toRegex()
val matchResult = pattern.find(line) val matchResult = pattern.find(line)
if (matchResult != null) { if (matchResult != null) {
val (scanTitle, scanUrl) = matchResult.destructured val (scanTitle, scanUrl) = matchResult.destructured
if (!scanUrl.contains("va")) { if (!scanUrl.contains("va")) {
val scanlatorGroup = scanTitle.replace(Regex("""(Scans|\(|\))"""), "").trim() val scanlatorGroup = scanTitle.replace(scanPattern, "").trim()
val fetchExistentSubMangas = GET( val fetchExistentSubMangas = GET(
url.newBuilder() url.newBuilder()
.addPathSegments( .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() return parsedChapterList.asReversed()
} }
@ -272,32 +316,19 @@ class AnimeSama : ParsedHttpSource() {
val title = url.queryParameter("title") val title = url.queryParameter("title")
val chapter = url.queryParameter("id") 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+))""") val apiNbChapImgResponse = client.newCall(requestToFetchChapters).execute()
.findAll(documentString) val apiNbChapImgJson = Json.decodeFromString<Map<String, Int>>(apiNbChapImgResponse.body.string())
.associate { match -> val imageCount = apiNbChapImgJson[chapter] ?: 0
val episode = match.groupValues[1].toInt()
val arrayContent = match.groupValues[2]
val explicitLength = match.groupValues[3]
val length = when { val imageList = (1..imageCount).map { index ->
explicitLength.isNotEmpty() -> explicitLength.toInt() Page(index, imageUrl = "$cdn$title/$chapter/$index.jpg")
arrayContent.isNotEmpty() -> arrayContent.split(Regex(",\\s*")).count { it.isNotBlank() } }.toMutableList()
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"),
)
}
return imageList 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)
}
}