{
+ for (pattern in seasonEpisodePatterns) {
+ val matchResult = pattern.find(fileName)
+ if (matchResult != null) {
+ val groups = matchResult.groupValues
+ return when (groups.count()) {
+ 3 -> Pair(groups[1].toIntOrNull(), groups[2].toIntOrNull()) // S01E01, S01 E01
+ 2 -> Pair(null, groups[1].toIntOrNull()) // Episode 1
+ 5 -> Pair(groups[1].toIntOrNull(), groups[3].toIntOrNull()) // Season 1 Episode 1
+ else -> Pair(null, null)
+ }
+ }
+ }
+ return Pair(null, null)
+ }
+
+ private fun extractYear(dateString: String?): Int? {
+ // If it is impossible to find a date in the given string,
+ // we can exit early
+ if (dateString == null || dateString.length < 4) return null
+
+ if (dateString.length == 4) {
+ // If the date is already a year (or it can not be one),
+ // we can exit early
+ return dateString.toIntOrNull()
+ }
+
+ val yearPattern = "\\b(\\d{4})\\b"
+ val yearRangePattern = "\\b(\\d{4})-(\\d{4})\\b"
+
+ // Check for year ranges like YYYY-YYYY and get the start year if a match is found
+ val yearRangeMatcher = Pattern.compile(yearRangePattern).matcher(dateString)
+ if (yearRangeMatcher.find()) {
+ return yearRangeMatcher.group(1)?.toInt()
+ }
+
+ // Check for single years within the date string in various formats
+ val yearMatcher = Pattern.compile(yearPattern).matcher(dateString)
+ if (yearMatcher.find()) {
+ return yearMatcher.group(1)?.toInt()
+ }
+
+ return null
+ }
+
+ private fun extractQuality(height: Int?): Int {
+ return when (height) {
+ Qualities.P144.value -> Qualities.P144.value
+ Qualities.P240.value -> Qualities.P240.value
+ Qualities.P360.value -> Qualities.P360.value
+ Qualities.P480.value -> Qualities.P480.value
+ Qualities.P720.value -> Qualities.P720.value
+ Qualities.P1080.value -> Qualities.P1080.value
+ Qualities.P1440.value -> Qualities.P1440.value
+ Qualities.P2160.value -> Qualities.P2160.value
+ else -> Qualities.Unknown.value
+ }
+ }
+
+ private fun getThumbnailUrl(fileName: String): String? {
+ val thumbnail = files.find {
+ it.format == "Thumbnail" && it.original == fileName
+ }
+ return thumbnail?.let { "https://$server$dir/${it.name}" }
+ }
+
+ private fun getCleanedName(fileName: String): String {
+ return fileName
+ .substringAfterLast('/')
+ .substringBeforeLast('.')
+ .replace('_', ' ')
+ }
+
+ private fun getUniqueName(fileName: String): String {
+ return getCleanedName(fileName)
+ // Some files have versions with very similar names.
+ // In this case, we do not want treat the files as
+ // separate when checking for uniqueness, otherwise it
+ // will think it is a playlist when that is not the case.
+ .substringBeforeLast(".")
+ .replace("512kb", "")
+ .trim()
+ }
+
+ private fun cleanHtml(html: String): String {
+ // We need to make sure descriptions use the correct text
+ // color/style of the rest of the app for consistency.
+ val document: Document = Jsoup.parse(html)
+ val fontTags: Elements = document.select("font")
+ for (fontTag: Element in fontTags) {
+ fontTag.unwrap()
+ }
+
+ // Strip the last tag to prevent to much padding
+ // towards the end.
+ val divTags: Elements = document.select("div")
+ if (divTags.isNotEmpty()) {
+ divTags.last()?.unwrap()
+ }
+
+ return document.body().html()
+ }
+
+ suspend fun toLoadResponse(provider: InternetArchiveProvider): LoadResponse {
+ val videoFiles = files.asSequence()
+ .filter {
+ it.lengthInSeconds >= 10.0 &&
+ (it.format.contains("MPEG", true) ||
+ it.format.startsWith("H.264", true) ||
+ it.format.startsWith("Matroska", true) ||
+ it.format.startsWith("DivX", true) ||
+ it.format.startsWith("Ogg Video", true))
+ }
+
+ val type = if (metadata.mediatype == "audio") {
+ TvType.Audio
+ } else TvType.Movie
+
+ return if (videoFiles.distinctBy { getUniqueName(it.name) }.count() <= 1 || type == TvType.Audio) {
+ // TODO if audio-playlist, use tracks
+ provider.newMovieLoadResponse(
+ metadata.title ?: metadata.identifier,
+ "${provider.mainUrl}/details/${metadata.identifier}",
+ type,
+ metadata.identifier
+ ) {
+ plot = metadata.description?.let { cleanHtml(it) }
+ year = extractYear(metadata.date)
+ tags = if (metadata.subject?.count() == 1) {
+ metadata.subject[0].split(";")
+ } else metadata.subject
+ posterUrl = "${provider.mainUrl}/services/img/${metadata.identifier}"
+ duration = ((videoFiles.firstOrNull()?.lengthInSeconds ?: 0f) / 60).roundToInt()
+ actors = metadata.creator?.map {
+ ActorData(Actor(it, ""), roleString = "Creator")
+ }
+ }
+ } else {
+ /**
+ * This may not be a TV series but we use it for video playlists as
+ * it is better for resuming (or downloading) what specific track
+ * you are on.
+ */
+ val urlMap = mutableMapOf>()
+
+ videoFiles.forEach { file ->
+ val cleanedName = getCleanedName(file.original ?: file.name)
+ val videoFileUrl = "https://$server$dir/${file.name}"
+ val fileQuality = extractQuality(file.height)
+ if (urlMap.containsKey(cleanedName)) {
+ urlMap[cleanedName]?.add(
+ URLData(
+ url = videoFileUrl,
+ format = file.format,
+ size = file.size ?: 0f,
+ quality = fileQuality
+ )
+ )
+ } else urlMap[cleanedName] = mutableSetOf(
+ URLData(
+ url = videoFileUrl,
+ format = file.format,
+ size = file.size ?: 0f,
+ quality = fileQuality
+ )
+ )
+ }
+
+ val mostFrequentLengthInMinutes = videoFiles
+ .map { (it.lengthInSeconds / 60).roundToInt() }
+ .groupBy { it }
+ .maxByOrNull { it.value.count() }
+ ?.key
+
+ val episodes = urlMap.map { (fileName, urlData) ->
+ val file = videoFiles.first { getCleanedName(it.name) == fileName }
+ val episodeInfo = extractEpisodeInfo(file.original ?: file.name)
+
+ provider.newEpisode(
+ LoadData(
+ urlData = urlData,
+ type = "video-playlist"
+ ).toJson()
+ ) {
+ name = file.title ?: fileName
+ season = episodeInfo.first
+ episode = episodeInfo.second
+ runTime = (file.lengthInSeconds / 60).roundToInt()
+ posterUrl = getThumbnailUrl(file.original ?: file.name)
+ }
+ }.sortedWith(compareBy({ it.season }, { it.episode }))
+
+ provider.newTvSeriesLoadResponse(
+ metadata.title ?: metadata.identifier,
+ "${provider.mainUrl}/details/${metadata.identifier}",
+ TvType.TvSeries,
+ episodes
+ ) {
+ plot = metadata.description?.let { cleanHtml(it) }
+ year = extractYear(metadata.date)
+ tags = if (metadata.subject?.count() == 1) {
+ metadata.subject[0].split(";")
+ } else metadata.subject
+ posterUrl = "${provider.mainUrl}/services/img/${metadata.identifier}"
+ duration = mostFrequentLengthInMinutes
+ actors = metadata.creator?.map {
+ ActorData(Actor(it, ""), roleString = "Creator")
+ }
+ }
+ }
+ }
+ }
+
+ private data class MediaEntry(
+ val identifier: String,
+ val mediatype: String,
+ val title: String?,
+ val description: String?,
+ val subject: List?,
+ val creator: List?,
+ val date: String?
+ )
+
+ private data class MediaFile(
+ val name: String,
+ val format: String,
+ val title: String?,
+ val original: String?,
+ val length: String?,
+ val size: Float?,
+ val height: Int?
+ ) {
+ val lengthInSeconds: Float by lazy { calculateLengthInSeconds() }
+
+ private fun calculateLengthInSeconds(): Float {
+ return length?.toFloatOrNull() ?: run {
+ // Check if length is in a different format and convert to seconds
+ if (length?.contains(":") == true) {
+ lengthToSeconds(length)
+ } else 0f
+ }
+ }
+
+ private fun lengthToSeconds(time: String): Float {
+ val parts = time.split(":")
+ return when (parts.count()) {
+ 2 -> {
+ val minutes = parts[0].toFloatOrNull() ?: 0f
+ val seconds = parts[1].toFloatOrNull() ?: 0f
+ (minutes * 60) + seconds
+ }
+ 3 -> {
+ val hours = parts[0].toFloatOrNull() ?: 0f
+ val minutes = parts[1].toFloatOrNull() ?: 0f
+ val seconds = parts[2].toFloatOrNull() ?: 0f
+ (hours * 3600) + (minutes * 60) + seconds
+ }
+ else -> 0f
+ }
+ }
+ }
+
+ data class LoadData(
+ val urlData: Set,
+ val type: String
+ )
+
+ data class URLData(
+ val url: String,
+ val format: String,
+ val size: Float,
+ val quality: Int
+ )
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val load = tryParseJson(data)
+ // TODO if audio-playlist, use tracks
+ if (load?.type == "video-playlist") {
+ val distinctURLData = load.urlData.filterNot {
+ it.format.endsWith("IA")
+ }
+
+ fun getName(format: String): String {
+ return if (distinctURLData.count() > 1) {
+ "$name ($format)"
+ } else name
+ }
+
+ distinctURLData.sortedByDescending { it.size }.forEach { urlData: URLData ->
+ callback(
+ ExtractorLink(
+ this.name,
+ getName(urlData.format),
+ urlData.url,
+ "",
+ urlData.quality
+ )
+ )
+ }
+ } else {
+ loadExtractor(
+ "https://archive.org/details/$data",
+ subtitleCallback,
+ callback
+ )
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/InvidiousProvider/build.gradle.kts b/InvidiousProvider/build.gradle.kts
index d3447bd6..44ab6754 100644
--- a/InvidiousProvider/build.gradle.kts
+++ b/InvidiousProvider/build.gradle.kts
@@ -1,4 +1,4 @@
-// use an integer for version numbers
+// Use an integer for version numbers
version = 7
cloudstream {
@@ -10,7 +10,7 @@ cloudstream {
/**
* Status int as one of the following:
* 0: Down
- * 1: Ok
+ * 1: Ok
* 2: Slow
* 3: Beta-only
**/
diff --git a/build.gradle.kts b/build.gradle.kts
index 3c98022d..aeef976c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -81,6 +81,9 @@ subprojects {
implementation(kotlin("stdlib")) // Adds Standard Kotlin Features
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
implementation("org.jsoup:jsoup:1.18.3") // HTML Parser
+ // IMPORTANT: Do not bump Jackson above 2.13.1, as newer versions will
+ // break compatibility on older Android devices.
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") // JSON Parser
}
}
From fc7bd5cab4001b187f40db2db8519c83bbc77a6c Mon Sep 17 00:00:00 2001
From: Luna712 <142361265+Luna712@users.noreply.github.com>
Date: Sun, 12 Jan 2025 10:56:52 -0700
Subject: [PATCH 2/5] Fix
---
InvidiousProvider/build.gradle.kts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvidiousProvider/build.gradle.kts b/InvidiousProvider/build.gradle.kts
index 44ab6754..50455c26 100644
--- a/InvidiousProvider/build.gradle.kts
+++ b/InvidiousProvider/build.gradle.kts
@@ -10,7 +10,7 @@ cloudstream {
/**
* Status int as one of the following:
* 0: Down
- * 1: Ok
+ * 1: Ok
* 2: Slow
* 3: Beta-only
**/
From 34c077a8c49a7201f6fae51ec87f9db60222365c Mon Sep 17 00:00:00 2001
From: Luna712 <142361265+Luna712@users.noreply.github.com>
Date: Mon, 6 Oct 2025 17:55:21 -0600
Subject: [PATCH 3/5] Update
---
InternetArchiveProvider/build.gradle.kts | 4 +++-
.../recloudstream/InternetArchivePlugin.kt | 7 +++---
.../recloudstream/InternetArchiveProvider.kt | 24 ++++++++++++-------
3 files changed, 21 insertions(+), 14 deletions(-)
diff --git a/InternetArchiveProvider/build.gradle.kts b/InternetArchiveProvider/build.gradle.kts
index 633dc614..093e0d29 100644
--- a/InternetArchiveProvider/build.gradle.kts
+++ b/InternetArchiveProvider/build.gradle.kts
@@ -1,5 +1,5 @@
// Use an integer for version numbers
-version = 20
+version = 1
cloudstream {
// All of these properties are optional, you can safely remove any of them.
@@ -18,4 +18,6 @@ cloudstream {
tvTypes = listOf("Others")
iconUrl = "https://www.google.com/s2/favicons?domain=archive.org&sz=%size%"
+
+ isCrossPlatform = true
}
\ No newline at end of file
diff --git a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchivePlugin.kt b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchivePlugin.kt
index 969f2d44..32734213 100644
--- a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchivePlugin.kt
+++ b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchivePlugin.kt
@@ -1,12 +1,11 @@
package recloudstream
-import android.content.Context
+import com.lagradost.cloudstream3.plugins.BasePlugin
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
-import com.lagradost.cloudstream3.plugins.Plugin
@CloudstreamPlugin
-class InternetArchivePlugin: Plugin() {
- override fun load(context: Context) {
+class InternetArchivePlugin: BasePlugin() {
+ override fun load() {
// All providers should be added in this manner. Please don't edit the providers list directly.
registerMainAPI(InternetArchiveProvider())
}
diff --git a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
index 47373733..4e8da99e 100644
--- a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
+++ b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
@@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchResponse
+import com.lagradost.cloudstream3.SearchResponseList
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
@@ -21,12 +22,14 @@ import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.newMovieLoadResponse
import com.lagradost.cloudstream3.newMovieSearchResponse
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
+import com.lagradost.cloudstream3.toNewSearchResponseList
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.encodeUri
import com.lagradost.cloudstream3.utils.loadExtractor
+import com.lagradost.cloudstream3.utils.newExtractorLink
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -69,14 +72,16 @@ class InternetArchiveProvider : MainAPI() {
}
}
- override suspend fun search(query: String): List {
+ override suspend fun search(query: String, page: Int): SearchResponseList? {
return try {
- val responseText = app.get("$mainUrl/advancedsearch.php?q=${query.encodeUri()}+mediatype:(movies OR audio)&fl[]=identifier&fl[]=title&fl[]=mediatype&rows=26&output=json").text
+ val responseText = app.get("$mainUrl/advancedsearch.php?q=${query.encodeUri()}+mediatype:(movies OR audio)&fl[]=identifier&fl[]=title&fl[]=mediatype&rows=26&page=$page&output=json").text
val res = tryParseJson(responseText)
- res?.response?.docs?.map { it.toSearchResponse(this) } ?: emptyList()
+ res?.response?.docs?.map {
+ it.toSearchResponse(this)
+ }?.toNewSearchResponseList()
} catch (e: Exception) {
logError(e)
- emptyList()
+ null
}
}
@@ -432,13 +437,14 @@ class InternetArchiveProvider : MainAPI() {
distinctURLData.sortedByDescending { it.size }.forEach { urlData: URLData ->
callback(
- ExtractorLink(
+ newExtractorLink(
this.name,
getName(urlData.format),
- urlData.url,
- "",
- urlData.quality
- )
+ urlData.url
+ ) {
+ quality = urlData.quality
+ referer = ""
+ }
)
}
} else {
From a5c3f1034bf69533642aa35c2d048eb8e8c55903 Mon Sep 17 00:00:00 2001
From: Luna712 <142361265+Luna712@users.noreply.github.com>
Date: Tue, 7 Oct 2025 13:59:29 -0600
Subject: [PATCH 4/5] Fix pagination for home page
---
.../src/main/kotlin/recloudstream/InternetArchiveProvider.kt | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
index 4e8da99e..be81d5d5 100644
--- a/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
+++ b/InternetArchiveProvider/src/main/kotlin/recloudstream/InternetArchiveProvider.kt
@@ -63,8 +63,7 @@ class InternetArchiveProvider : MainAPI() {
newHomePageResponse(
listOf(
HomePageList("Featured", homePageList, true)
- ),
- false
+ )
)
} catch (e: Exception) {
logError(e)
From 03d3f4403a6c64ac55a2d1994eaaa09f559b3417 Mon Sep 17 00:00:00 2001
From: Luna712 <142361265+Luna712@users.noreply.github.com>
Date: Sat, 1 Nov 2025 10:07:29 -0600
Subject: [PATCH 5/5] Update
---
DailymotionProvider/build.gradle.kts | 12 ++++++------
InternetArchiveProvider/build.gradle.kts | 12 ++++++------
InvidiousProvider/build.gradle.kts | 12 ++++++------
TwitchProvider/build.gradle.kts | 12 ++++++------
4 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/DailymotionProvider/build.gradle.kts b/DailymotionProvider/build.gradle.kts
index a4e93767..3a44d15b 100644
--- a/DailymotionProvider/build.gradle.kts
+++ b/DailymotionProvider/build.gradle.kts
@@ -8,12 +8,12 @@ cloudstream {
authors = listOf("Luna712")
/**
- * Status int as one of the following:
- * 0: Down
- * 1: Ok
- * 2: Slow
- * 3: Beta-only
- **/
+ * Status int as one of the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta-only
+ */
status = 1 // Will be 3 if unspecified
tvTypes = listOf("Others")
diff --git a/InternetArchiveProvider/build.gradle.kts b/InternetArchiveProvider/build.gradle.kts
index 093e0d29..9cc4f04b 100644
--- a/InternetArchiveProvider/build.gradle.kts
+++ b/InternetArchiveProvider/build.gradle.kts
@@ -8,12 +8,12 @@ cloudstream {
authors = listOf("Luna712")
/**
- * Status int as one of the following:
- * 0: Down
- * 1: Ok
- * 2: Slow
- * 3: Beta-only
- **/
+ * Status int as one of the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta-only
+ */
status = 1 // Will be 3 if unspecified
tvTypes = listOf("Others")
diff --git a/InvidiousProvider/build.gradle.kts b/InvidiousProvider/build.gradle.kts
index eb53677c..ee08aa7f 100644
--- a/InvidiousProvider/build.gradle.kts
+++ b/InvidiousProvider/build.gradle.kts
@@ -8,12 +8,12 @@ cloudstream {
authors = listOf("Cloudburst")
/**
- * Status int as one of the following:
- * 0: Down
- * 1: Ok
- * 2: Slow
- * 3: Beta-only
- **/
+ * Status int as one of the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta-only
+ */
status = 1 // Will be 3 if unspecified
tvTypes = listOf("Others")
diff --git a/TwitchProvider/build.gradle.kts b/TwitchProvider/build.gradle.kts
index 0c8863af..536b02ea 100644
--- a/TwitchProvider/build.gradle.kts
+++ b/TwitchProvider/build.gradle.kts
@@ -8,12 +8,12 @@ cloudstream {
authors = listOf("CranberrySoup")
/**
- * Status int as one of the following:
- * 0: Down
- * 1: Ok
- * 2: Slow
- * 3: Beta-only
- **/
+ * Status int as one of the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta-only
+ */
status = 1 // Will be 3 if unspecified
tvTypes = listOf("Live")