Skip to content

Commit b2cfddc

Browse files
BobFlarcuong-trangemini-code-assist[bot]
authored
(fr/AnimeSama) Add VidMoly Extractor and update base Url (#8)
* Add VidMolyExtractor class for video extraction * Bump version code from 21 to 22 * Add intent filter for anime-sama.tv * Integrate VidMolyExtractor and update base URL handling Added VidMolyExtractor for video extraction and updated preferences for base URL. * Refactor imports in AnimeSama.kt Removed redundant import of VidMolyExtractor. * Fix formatting in AnimeSama.kt * Add import for EditTextPreference in AnimeSama.kt * lint * Extract host from url * Auto update domain * refactor * Handle m3u8 empty lines Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * refactor --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 47b8e56 commit b2cfddc

File tree

5 files changed

+169
-17
lines changed

5 files changed

+169
-17
lines changed

src/all/streamingcommunity/src/eu/kanade/tachiyomi/animeextension/all/streamingcommunity/StreamingCommunity.kt

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class StreamingCommunity(override val lang: String, private val showType: String
5656

5757
private var SharedPreferences.customDomain by preferences.delegate(PREF_CUSTOM_DOMAIN_KEY, DOMAIN_DEFAULT)
5858

59-
private var homepage by LazyMutable { preferences.customDomain.ifBlank { DOMAIN_DEFAULT } }
59+
private var homepage by LazyMutable { preferences.customDomain.ifBlank { DOMAIN_DEFAULT }.sanitizeDomain() }
6060

6161
override val client: OkHttpClient = super.client.newBuilder()
6262
.followRedirects(false)
@@ -68,7 +68,7 @@ class StreamingCommunity(override val lang: String, private val showType: String
6868

6969
while (response.isRedirect && redirectCount < maxRedirects) {
7070
val newUrl = response.header("Location") ?: break
71-
val newUrlHttp = newUrl.toHttpUrl()
71+
val newUrlHttp = request.url.resolve(newUrl) ?: break
7272
val redirectedDomain = newUrlHttp.run { "$scheme://$host" }
7373
if (redirectedDomain != homepage) {
7474
updateDomain(redirectedDomain)
@@ -77,7 +77,7 @@ class StreamingCommunity(override val lang: String, private val showType: String
7777
request = request.newBuilder()
7878
.url(newUrlHttp)
7979
.apply {
80-
apiHeaders["Host"]?.let { header("Host", it) }
80+
apiHeaders["Origin"]?.let { header("Origin", it) }
8181
apiHeaders["Referer"]?.let { header("Referer", it) }
8282
}
8383
.build()
@@ -107,14 +107,14 @@ class StreamingCommunity(override val lang: String, private val showType: String
107107

108108
private val apiHeadersRef by lazy { AtomicReference(newApiHeader()) }
109109
private fun newApiHeader() = headers.newBuilder()
110-
.add("Host", baseUrl.toHttpUrl().host)
111-
.add("Referer", baseUrl)
110+
.add("Origin", homepage)
111+
.add("Referer", "$homepage/")
112112
.build()
113113

114114
private val jsonHeadersRef by lazy { AtomicReference(newJsonHeader()) }
115115
private fun newJsonHeader() = headers.newBuilder()
116-
.add("Host", baseUrl.toHttpUrl().host)
117-
.add("Referer", baseUrl)
116+
.add("Origin", homepage)
117+
.add("Referer", "$homepage/")
118118
.add("Content-Type", "application/json")
119119
.add("X-Requested-With", "XMLHttpRequest")
120120
.add("X-Inertia", "true")
@@ -510,8 +510,10 @@ class StreamingCommunity(override val lang: String, private val showType: String
510510

511511
// ============================== Settings ==============================
512512

513+
private fun String.sanitizeDomain() = trim().removeSuffix("/").ifBlank { DOMAIN_DEFAULT }
514+
513515
private fun updateDomain(domain: String) {
514-
val newDomain = domain.trim().removeSuffix("/").ifBlank { DOMAIN_DEFAULT }
516+
val newDomain = domain.sanitizeDomain()
515517
if (URLUtil.isValidUrl(newDomain)) {
516518
Log.i(TAG, "Updating domain to: $newDomain")
517519
preferences.customDomain = newDomain
@@ -546,8 +548,8 @@ class StreamingCommunity(override val lang: String, private val showType: String
546548
val newDomain = newValue.trim().removeSuffix("/")
547549
if (newDomain.isBlank() || URLUtil.isValidUrl(newDomain)) {
548550
updateDomain(newDomain)
549-
// this `true` will update the preference to empty string if the new value is blank & override domain set in `updateDomain`,
550-
// so make sure to guard `homepage` against blank values.
551+
// this `true` will update the preference to empty string if the new value is blank &
552+
// override domain set in `updateDomain`, so make sure to guard `homepage` against blank values.
551553
// But it's needed to update the preference summary.
552554
true
553555
} else {

src/fr/animesama/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
android:host="anime-sama.si"
1717
android:pathPattern="/catalogue/..*"
1818
android:scheme="https" />
19+
<data
20+
android:host="anime-sama.tv"
21+
android:pathPattern="/catalogue/..*"
22+
android:scheme="https" />
1923
</intent-filter>
2024
</activity>
2125
</application>

src/fr/animesama/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
ext {
22
extName = 'Anime-Sama'
33
extClass = '.AnimeSama'
4-
extVersionCode = 21
4+
extVersionCode = 22
55
}
66

77
apply from: "$rootDir/common.gradle"

src/fr/animesama/src/eu/kanade/tachiyomi/animeextension/fr/animesama/AnimeSama.kt

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package eu.kanade.tachiyomi.animeextension.fr.animesama
22

3+
import android.content.SharedPreferences
4+
import android.webkit.URLUtil
5+
import android.widget.Toast
36
import androidx.preference.ListPreference
47
import androidx.preference.PreferenceScreen
58
import app.cash.quickjs.QuickJs
9+
import eu.kanade.tachiyomi.animeextension.fr.animesama.extractors.VidMolyExtractor
610
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
711
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
812
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@@ -17,10 +21,14 @@ import eu.kanade.tachiyomi.network.GET
1721
import eu.kanade.tachiyomi.util.asJsoup
1822
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
1923
import eu.kanade.tachiyomi.util.parallelFlatMapBlocking
24+
import keiyoushi.utils.LazyMutable
25+
import keiyoushi.utils.addEditTextPreference
26+
import keiyoushi.utils.delegate
2027
import keiyoushi.utils.getPreferencesLazy
2128
import kotlinx.serialization.encodeToString
2229
import kotlinx.serialization.json.Json
2330
import okhttp3.HttpUrl.Companion.toHttpUrl
31+
import okhttp3.OkHttpClient
2432
import okhttp3.Request
2533
import okhttp3.Response
2634
import uy.kohesive.injekt.injectLazy
@@ -31,17 +39,51 @@ class AnimeSama :
3139

3240
override val name = "Anime-Sama"
3341

34-
// Domain info at: https://anime-sama.pw
35-
override val baseUrl = "https://anime-sama.si"
36-
3742
override val lang = "fr"
3843

3944
override val supportsLatest = true
4045

41-
private val json: Json by injectLazy()
42-
4346
private val preferences by getPreferencesLazy()
4447

48+
private var SharedPreferences.customDomain by preferences.delegate(PREF_URL_KEY, PREF_URL_DEFAULT)
49+
50+
override var baseUrl by LazyMutable { preferences.customDomain.ifBlank { PREF_URL_DEFAULT }.sanitizeDomain() }
51+
52+
override val client: OkHttpClient = super.client.newBuilder()
53+
.followRedirects(false)
54+
.addInterceptor { chain ->
55+
val maxRedirects = 5
56+
var request = chain.request()
57+
var response = chain.proceed(request)
58+
var redirectCount = 0
59+
60+
while (response.isRedirect && redirectCount < maxRedirects) {
61+
val newUrl = response.header("Location") ?: break
62+
val newUrlHttp = request.url.resolve(newUrl) ?: break
63+
val redirectedDomain = newUrlHttp.run { "$scheme://$host" }
64+
if (redirectedDomain != baseUrl) {
65+
updateDomain(redirectedDomain)
66+
}
67+
response.close()
68+
request = request.newBuilder()
69+
.url(newUrlHttp)
70+
.apply {
71+
header("Origin", redirectedDomain)
72+
header("Referer", "$redirectedDomain/")
73+
}
74+
.build()
75+
response = chain.proceed(request)
76+
redirectCount++
77+
}
78+
if (redirectCount >= maxRedirects) {
79+
response.close()
80+
throw java.io.IOException("Too many redirects: $maxRedirects")
81+
}
82+
response
83+
}.build()
84+
85+
private val json: Json by injectLazy()
86+
4587
// ============================== Popular ===============================
4688
override fun popularAnimeParse(response: Response): AnimesPage {
4789
val doc = response.asJsoup()
@@ -115,6 +157,7 @@ class AnimeSama :
115157
private val sibnetExtractor by lazy { SibnetExtractor(client) }
116158
private val vkExtractor by lazy { VkExtractor(client, headers) }
117159
private val sendvidExtractor by lazy { SendvidExtractor(client, headers) }
160+
private val vidmolyExtractor by lazy { VidMolyExtractor(client, headers) }
118161

119162
override suspend fun getVideoList(episode: SEpisode): List<Video> {
120163
val playerUrls = json.decodeFromString<List<List<String>>>(episode.url)
@@ -124,8 +167,14 @@ class AnimeSama :
124167
with(playerUrl) {
125168
when {
126169
contains("sibnet.ru") -> sibnetExtractor.videosFromUrl(playerUrl, prefix)
170+
127171
contains("vk.") -> vkExtractor.videosFromUrl(playerUrl, prefix)
172+
128173
contains("sendvid.com") -> sendvidExtractor.videosFromUrl(playerUrl, prefix)
174+
175+
// .to doesn't work, and it's .biz that's used on the site
176+
contains("vidmoly") -> vidmolyExtractor.videosFromUrl(playerUrl.replace(".to", ".biz"), prefix)
177+
129178
else -> emptyList()
130179
}
131180
}
@@ -143,8 +192,8 @@ class AnimeSama :
143192
return this.sortedWith(
144193
compareBy(
145194
{ it.quality.contains(voices, true) },
146-
{ it.quality.contains(quality) },
147195
{ it.quality.contains(player, true) },
196+
{ it.quality.contains(quality) },
148197
),
149198
).reversed()
150199
}
@@ -225,7 +274,34 @@ class AnimeSama :
225274
return List(urls[0].size) { i -> urls.mapNotNull { it.getOrNull(i) }.distinct() }
226275
}
227276

277+
private fun String.sanitizeDomain() = trim().removeSuffix("/").ifBlank { PREF_URL_DEFAULT }
278+
279+
private fun updateDomain(domain: String) {
280+
val newDomain = domain.sanitizeDomain()
281+
if (URLUtil.isValidUrl(newDomain)) {
282+
preferences.customDomain = newDomain
283+
baseUrl = newDomain
284+
}
285+
}
286+
228287
override fun setupPreferenceScreen(screen: PreferenceScreen) {
288+
screen.addEditTextPreference(
289+
key = PREF_URL_KEY,
290+
title = PREF_URL_TITLE,
291+
default = PREF_URL_DEFAULT,
292+
summary = PREF_URL_SUMMARY,
293+
onChange = { _, newValue ->
294+
val newDomain = newValue.trim().removeSuffix("/")
295+
if (URLUtil.isValidUrl(newDomain)) {
296+
updateDomain(newDomain)
297+
true
298+
} else {
299+
Toast.makeText(screen.context, "URL invalide. Exemple: $PREF_URL_DEFAULT", Toast.LENGTH_LONG).show()
300+
false
301+
}
302+
},
303+
)
304+
229305
ListPreference(screen.context).apply {
230306
key = PREF_QUALITY_KEY
231307
title = "Preferred quality"
@@ -257,6 +333,13 @@ class AnimeSama :
257333
companion object {
258334
const val PREFIX_SEARCH = "id:"
259335

336+
private const val PREF_URL_KEY = "base_url_pref"
337+
private const val PREF_URL_TITLE = "URL de base"
338+
339+
// Domain info at: https://anime-sama.pw
340+
private const val PREF_URL_DEFAULT = "https://anime-sama.tv"
341+
private const val PREF_URL_SUMMARY = "Pour changer le domaine de l'extension. Voir https://anime-sama.pw"
342+
260343
private val voicesMap = mapOf(
261344
"Préférer VOSTFR" to "vostfr",
262345
"Préférer VF" to "vf",
@@ -275,6 +358,7 @@ class AnimeSama :
275358
"Sendvid" to "sendvid",
276359
"Sibnet" to "sibnet",
277360
"VK" to "vk",
361+
"VidMoly" to "vidmoly",
278362
)
279363
private val PLAYERS = playersMap.keys.toTypedArray()
280364
private val PLAYERS_VALUES = playersMap.values.toTypedArray()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package eu.kanade.tachiyomi.animeextension.fr.animesama.extractors
2+
3+
import eu.kanade.tachiyomi.animesource.model.Video
4+
import eu.kanade.tachiyomi.network.GET
5+
import eu.kanade.tachiyomi.network.awaitSuccess
6+
import eu.kanade.tachiyomi.util.parallelFlatMap
7+
import okhttp3.Headers
8+
import okhttp3.HttpUrl.Companion.toHttpUrl
9+
import okhttp3.OkHttpClient
10+
11+
class VidMolyExtractor(private val client: OkHttpClient, private val headers: Headers) {
12+
13+
private val playerRegex = Regex("""player\.setup\(\s*\{([\s\S]*?)\}\s*\);""")
14+
private val fileRegex = Regex("""file\s*:\s*["'](.*?)["']""")
15+
private val hlsResolutionRegex = Regex("""RESOLUTION=\d+x(\d+)""")
16+
17+
suspend fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
18+
val host = url.toHttpUrl().run {
19+
"$scheme://$host"
20+
}
21+
22+
val commonHeaders = headers.newBuilder()
23+
.set("Referer", "$host/")
24+
.set("Origin", host)
25+
.build()
26+
27+
return runCatching {
28+
client.newCall(GET(url, commonHeaders)).awaitSuccess().use { response ->
29+
val html = response.body.string()
30+
val playerConfig = playerRegex.find(html)?.groupValues?.get(1) ?: return@runCatching emptyList()
31+
32+
fileRegex.findAll(playerConfig)
33+
.map { it.groupValues[1] }
34+
.filter { it.contains(".m3u8") }
35+
.toList()
36+
.parallelFlatMap { m3u8Url ->
37+
extractVideosFromPlaylist(m3u8Url, commonHeaders, prefix)
38+
}
39+
}
40+
}.getOrDefault(emptyList())
41+
}
42+
43+
private suspend fun extractVideosFromPlaylist(playlistUrl: String, headers: Headers, prefix: String): List<Video> {
44+
return runCatching {
45+
client.newCall(GET(playlistUrl, headers)).awaitSuccess().use { response ->
46+
val content = response.body.string()
47+
48+
if (!content.contains("#EXT-X-STREAM-INF")) {
49+
return listOf(Video(playlistUrl, "${prefix}VidMoly - Default", playlistUrl, headers = headers))
50+
}
51+
52+
content.split("#EXT-X-STREAM-INF").drop(1).mapNotNull { variant ->
53+
val quality = hlsResolutionRegex.find(variant)?.groupValues?.get(1)?.let { "${it}p" } ?: "Unknown"
54+
variant.lines().drop(1).firstOrNull(String::isNotBlank)?.trim()?.let { url ->
55+
val videoUrl = if (url.startsWith("http")) url else playlistUrl.toHttpUrl().resolve(url)?.toString() ?: return@mapNotNull null
56+
Video(videoUrl, "${prefix}VidMoly - $quality", videoUrl, headers = headers)
57+
}
58+
}
59+
}
60+
}.getOrDefault(emptyList())
61+
}
62+
}

0 commit comments

Comments
 (0)