Compare commits

...

12 Commits

Author SHA1 Message Date
RowdyRushya ce3359c0fa
silly change 2024-04-24 17:59:15 -07:00
RowdyRushya 30839e3a09
Merge branch 'recloudstream:master' into master 2024-04-24 17:56:49 -07:00
RowdyRushya c363596d4f
Including series name in episode data.
This data is passed to loadLinks but name is not included in TvSeriesLoadResponse which is crucial as not all sites support filtering using ids and name is required.
2024-04-24 17:56:16 -07:00
RowdyRushya dc8ba3399c
Learned and understood what is flattening the code. 2024-04-24 17:49:00 -07:00
int3debug e6b9d621f9
feat(ui): added option to reset sub delay (#1041) 2024-04-22 17:00:27 +02:00
KingLucius 0019f85501
Trakt meta provider for extensions (#1026) 2024-04-22 16:59:14 +02:00
IndusAryan 0744189020
feat(ui): show account name and image on main settings page (#1001) 2024-04-22 16:48:54 +02:00
Ömer Faruk Sancak 4399a612df
Update Vidmoly.kt (#1051) 2024-04-22 01:14:36 +02:00
KingLucius e01ff4d843
Fix NewPipeExtractor lib path (#1050) 2024-04-22 01:13:55 +02:00
KingLucius 6cef9f7ea2
Filtering first unwatched episode respects watched state (#1049) 2024-04-20 22:18:49 +02:00
int3debug 9a18ef6411
bugfix: fixing regex special chars break it (#1047) 2024-04-17 23:48:33 +02:00
CranberrySoup 6df3ef14f6
First steps for multiplatform API (#1003)
* First steps for multiplatform api

* Buildconfig testing

* Fix publishing and classes.jar

* Update build.gradle.kts
2024-04-16 23:07:28 +02:00
24 changed files with 707 additions and 80 deletions

View File

@ -4,17 +4,16 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/library" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -1,5 +1,6 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import java.net.URL
@ -13,6 +14,7 @@ plugins {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
var isLibraryDebug = false
fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec {
@ -103,6 +105,7 @@ android {
)
}
debug {
isLibraryDebug = true
isDebuggable = true
applicationIdSuffix = ".debug"
proguardFiles(
@ -199,7 +202,7 @@ dependencies {
// PlayBack
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers
implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
@ -232,18 +235,37 @@ dependencies {
implementation("androidx.work:work-runtime:2.9.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
implementation(project(":library") {
this.extra.set("isDebug", isLibraryDebug)
})
}
tasks.register("androidSourcesJar", Jar::class) {
tasks.register<Jar>("androidSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
}
// For GradLew Plugin
tasks.register("makeJar", Copy::class) {
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
into("build")
include("classes.jar")
tasks.register<Copy>("copyJar") {
from(
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
"../library/build/libs"
)
into("build/app-classes")
include("classes.jar", "library-jvm*.jar")
// Remove the version
rename("library-jvm.*.jar", "library-jvm.jar")
}
// Merge the app classes and the library classes into classes.jar
tasks.register<Jar>("makeJar") {
dependsOn(tasks.getByName("copyJar"))
from(
zipTree("build/app-classes/classes.jar"),
zipTree("build/app-classes/library-jvm.jar")
)
destinationDirectory.set(layout.buildDirectory)
archivesName = "classes"
}
tasks.withType<KotlinCompile> {

View File

@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String {
}
}
class ErrorLoadingException(message: String? = null) : Exception(message)
fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) {
return null

View File

@ -22,17 +22,15 @@ class VidSrcTo : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id")?.let { mediaId ->
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>()
if (res?.status == 200) {
res.result?.amap { source ->
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>()
val finalUrl = DecryptUrl(embedRes?.result?.encUrl ?: "")
when (source.title) {
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
}
}
val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>() ?: return
if (res?.status != 200) return
res.result?.amap { source ->
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
val finalUrl = DecryptUrl(embedRes?.result?.encUrl)
when (source.title) {
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
}
}
}
@ -46,6 +44,7 @@ class VidSrcTo : ExtractorApi() {
data = cipher.doFinal(data)
return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8")
}
data class VidsrctoEpisodeSources(
@JsonProperty("status") val status: Int,
@JsonProperty("result") val result: List<VidsrctoResult>?

View File

@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val headers = mapOf(
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
"Sec-Fetch-Dest" to "iframe"
)
val script = app.get(
url,
headers = headers,
referer = referer,
).document.select("script")
.find { it.data().contains("sources:") }?.data()
@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
@JsonProperty("kind") val kind: String? = null,
)
}
}

View File

@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
this.id,
episode.episode_number,
episode.season_number,
this.name ?: this.original_name,
).toJson(),
episode.name,
episode.season_number,
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
this.id,
episodeNum,
season.season_number,
this.name ?: this.original_name,
).toJson(),
season = season.season_number
)

View File

@ -0,0 +1,430 @@
package com.lagradost.cloudstream3.metaproviders
import android.net.Uri
import com.lagradost.cloudstream3.*
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import java.util.Locale
import java.text.SimpleDateFormat
import kotlin.math.roundToInt
open class TraktProvider : MainAPI() {
override var name = "Trakt"
override val hasMainPage = true
override val providerType = ProviderType.MetaProvider
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.Anime,
)
private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
override val mainPage = mainPageOf(
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
)
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
element.toSearchResponse()
}
return newHomePageResponse(request.name, results)
}
private fun MediaDetails.toSearchResponse(): SearchResponse {
val media = this.media ?: this
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
val poster = media.images?.poster?.firstOrNull()
if (mediaType == TvType.Movie) {
return newMovieSearchResponse(
name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.Movie,
) {
posterUrl = fixPath(poster)
}
} else {
return newTvSeriesSearchResponse(
name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.TvSeries,
) {
this.posterUrl = fixPath(poster)
}
}
}
override suspend fun search(query: String): List<SearchResponse>? {
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
element.toSearchResponse()
}
return results
}
override suspend fun load(url: String): LoadResponse {
val data = parseJson<Data>(url)
val mediaDetails = data.mediaDetails
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
val actors = parseJson<People>(resActor).cast?.map {
ActorData(
Actor(
name = it.person?.name!!,
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
),
roleString = it.character
)
}
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
val isBollywood = mediaDetails?.country == "in"
if (data.type == TvType.Movie) {
val linkData = LinkData(
id = mediaDetails?.ids?.tmdb,
imdbId = mediaDetails?.ids?.imdb.toString(),
tvdbId = mediaDetails?.ids?.tvdb,
type = data.type.toString(),
title = mediaDetails?.title,
year = mediaDetails?.year,
orgTitle = mediaDetails?.title,
isAnime = isAnime,
//jpTitle = later if needed as it requires another network request,
airedDate = mediaDetails?.released
?: mediaDetails?.firstAired,
isAsian = isAsian,
isBollywood = isBollywood,
).toJson()
return newMovieLoadResponse(
name = mediaDetails?.title!!,
url = data.toJson(),
dataUrl = linkData.toJson(),
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
) {
this.name = mediaDetails.title
this.apiName = "Trakt"
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
addTMDbId(mediaDetails.ids?.tmdb.toString())
}
} else {
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
val episodes = mutableListOf<Episode>()
val seasons = parseJson<List<Seasons>>(resSeasons)
val seasonsNames = mutableListOf<SeasonData>()
seasons.forEach { season ->
seasonsNames.add(
SeasonData(
season.number!!,
season.title
)
)
season.episodes?.map { episode ->
val linkData = LinkData(
id = mediaDetails?.ids?.tmdb,
imdbId = mediaDetails?.ids?.imdb.toString(),
tvdbId = mediaDetails?.ids?.tvdb,
type = data.type.toString(),
season = episode.season,
episode = episode.number,
title = mediaDetails?.title,
year = mediaDetails?.year,
orgTitle = mediaDetails?.title,
isAnime = isAnime,
airedYear = mediaDetails?.year,
lastSeason = seasons.size,
epsTitle = episode.title,
//jpTitle = later if needed as it requires another network request,
date = episode.firstAired,
airedDate = episode.firstAired,
isAsian = isAsian,
isBollywood = isBollywood,
isCartoon = isCartoon
).toJson()
episodes.add(
Episode(
data = linkData.toJson(),
name = episode.title,
season = episode.season,
episode = episode.number,
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
rating = episode.rating?.times(10)?.roundToInt(),
description = episode.overview,
).apply {
this.addDate(episode.firstAired)
}
)
}
}
return newTvSeriesLoadResponse(
name = mediaDetails?.title!!,
url = data.toJson(),
type = if (isAnime) TvType.Anime else TvType.TvSeries,
episodes = episodes
) {
this.name = mediaDetails.title
this.apiName = "Trakt"
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
this.episodes = episodes
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
this.showStatus = getStatus(mediaDetails.status)
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.seasonNames = seasonsNames
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
addTMDbId(mediaDetails.ids?.tmdb.toString())
}
}
}
private suspend fun getApi(url: String) : String {
return app.get(
url = url,
headers = mapOf(
"Content-Type" to "application/json",
"trakt-api-version" to "2",
"trakt-api-key" to traktClientId,
)
).toString()
}
private fun isUpcoming(dateString: String?): Boolean {
return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
APIHolder.unixTimeMS < dateTime
} catch (t: Throwable) {
logError(t)
false
}
}
private fun getStatus(t: String?): ShowStatus {
return when (t) {
"returning series" -> ShowStatus.Ongoing
"continuing" -> ShowStatus.Ongoing
else -> ShowStatus.Completed
}
}
private fun fixPath(url: String?): String? {
url ?: return null
return "https://$url"
}
private fun getWidthImageUrl(path: String?, width: String) : String? {
if (path == null) return null
if (!path.contains("image.tmdb.org")) return fixPath(path)
val fileName = Uri.parse(path).lastPathSegment ?: return null
return "https://image.tmdb.org/t/p/${width}/${fileName}"
}
private fun getOriginalWidthImageUrl(path: String?) : String? {
if (path == null) return null
if (!path.contains("image.tmdb.org")) return fixPath(path)
return getWidthImageUrl(path, "original")
}
data class Data(
val type: TvType? = null,
val mediaDetails: MediaDetails? = null,
)
data class MediaDetails(
@JsonProperty("title") val title: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("tagline") val tagline: String? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("released") val released: String? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("country") val country: String? = null,
@JsonProperty("updatedAt") val updatedAt: String? = null,
@JsonProperty("trailer") val trailer: String? = null,
@JsonProperty("homepage") val homepage: String? = null,
@JsonProperty("status") val status: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("votes") val votes: Long? = null,
@JsonProperty("comment_count") val commentCount: Long? = null,
@JsonProperty("language") val language: String? = null,
@JsonProperty("languages") val languages: List<String>? = null,
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
@JsonProperty("genres") val genres: List<String>? = null,
@JsonProperty("certification") val certification: String? = null,
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("airs") val airs: Airs? = null,
@JsonProperty("network") val network: String? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
)
data class Airs(
@JsonProperty("day") val day: String? = null,
@JsonProperty("time") val time: String? = null,
@JsonProperty("timezone") val timezone: String? = null,
)
data class Ids(
@JsonProperty("trakt") val trakt: Int? = null,
@JsonProperty("slug") val slug: String? = null,
@JsonProperty("tvdb") val tvdb: Int? = null,
@JsonProperty("imdb") val imdb: String? = null,
@JsonProperty("tmdb") val tmdb: Int? = null,
@JsonProperty("tvrage") val tvrage: String? = null,
)
data class Images(
@JsonProperty("fanart") val fanart: List<String>? = null,
@JsonProperty("poster") val poster: List<String>? = null,
@JsonProperty("logo") val logo: List<String>? = null,
@JsonProperty("clearart") val clearart: List<String>? = null,
@JsonProperty("banner") val banner: List<String>? = null,
@JsonProperty("thumb") val thumb: List<String>? = null,
@JsonProperty("screenshot") val screenshot: List<String>? = null,
@JsonProperty("headshot") val headshot: List<String>? = null,
)
data class People(
@JsonProperty("cast") val cast: List<Cast>? = null,
)
data class Cast(
@JsonProperty("character") val character: String? = null,
@JsonProperty("characters") val characters: List<String>? = null,
@JsonProperty("episode_count") val episodeCount: Long? = null,
@JsonProperty("person") val person: Person? = null,
@JsonProperty("images") val images: Images? = null,
)
data class Person(
@JsonProperty("name") val name: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
)
data class Seasons(
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
@JsonProperty("episode_count") val episodeCount: Int? = null,
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("network") val network: String? = null,
@JsonProperty("number") val number: Int? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("updated_at") val updatedAt: String? = null,
@JsonProperty("votes") val votes: Int? = null,
)
data class TraktEpisode(
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
@JsonProperty("comment_count") val commentCount: Int? = null,
@JsonProperty("episode_type") val episodeType: String? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("number") val number: Int? = null,
@JsonProperty("number_abs") val numberAbs: Int? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("updated_at") val updatedAt: String? = null,
@JsonProperty("votes") val votes: Int? = null,
)
data class LinkData(
val id: Int? = null,
val imdbId: String? = null,
val tvdbId: Int? = null,
val type: String? = null,
val season: Int? = null,
val episode: Int? = null,
val aniId: String? = null,
val animeId: String? = null,
val title: String? = null,
val year: Int? = null,
val orgTitle: String? = null,
val isAnime: Boolean = false,
val airedYear: Int? = null,
val lastSeason: Int? = null,
val epsTitle: String? = null,
val jpTitle: String? = null,
val date: String? = null,
val airedDate: String? = null,
val isAsian: Boolean = false,
val isBollywood: Boolean = false,
val isCartoon: Boolean = false,
)
}

View File

@ -0,0 +1,16 @@
package com.lagradost.cloudstream3.mvvm
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { it?.let { t -> action(t) } }
}
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}

View File

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.AnimatedVectorDrawable
import android.media.metrics.PlaybackErrorEvent
@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment
import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import androidx.media3.ui.*
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.rubensousa.previewseekbar.PreviewBar
@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment(
is VideoEndedEvent -> {
context?.let { ctx ->
// Resets subtitle delay on ended video
player.setSubtitleOffset(0)
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(ctx)
?.getBoolean(

View File

@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer {
}
Player.STATE_ENDED -> {
// Resets subtitle delay on ended video
setSubtitleOffset(0)
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context)
?.getBoolean(

View File

@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() {
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) {
val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f }
val lastWatchedIndex = episodes.value.indexOfLast { ep ->
ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched
}
val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() }
if (firstUnwatched != null) {

View File

@ -1,13 +1,13 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringRes
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.preference.Preference
@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
@ -133,7 +135,6 @@ class SettingsFragment : Fragment() {
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.main_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -141,21 +142,44 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle())
}
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
/** used to debug leaks
showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} :
${VideoDownloadManager.downloadProgressEvent.size}") **/
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
if (binding?.settingsProfilePic?.setImage(
pic,
errorImageDrawable = HomeFragment.errorProfilePic
) == true
) {
binding?.settingsProfileText?.text = login.name
binding?.settingsProfile?.isVisible = true
break
fun hasProfilePictureFromAccountManagers(accountManagers: List<AccountManager>): Boolean {
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
if (binding?.settingsProfilePic?.setImage(
pic,
errorImageDrawable = HomeFragment.errorProfilePic
) == true
) {
binding?.settingsProfileText?.text = login.name
return true // sync profile exists
}
}
return false // not syncing
}
// display local account information if not syncing
if (!hasProfilePictureFromAccountManagers(accountManagers)) {
val activity = activity ?: return
val currentAccount = try {
DataStoreHelper.accounts.firstOrNull {
it.keyIndex == DataStoreHelper.selectedKeyIndex
} ?: activity.let { DataStoreHelper.getDefaultAccount(activity) }
} catch (t: IllegalStateException) {
Log.e("AccountManager", "Activity not found", t)
null
}
binding?.settingsProfilePic?.setImage(currentAccount?.image)
binding?.settingsProfileText?.text = currentAccount?.name
}
binding?.apply {
listOf(
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,

View File

@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) {
throw Exception("Unknown p.a.c.k.e.r. encoding")
}
val unbase = Unbase(radix)
p = Pattern.compile("\\b\\w+\\b")
p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""")
m = p.matcher(payload)
val decoded = StringBuilder(payload)
var replaceOffset = 0

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/white">
<item>
<shape android:shape="oval">
<stroke
android:width="2dp"
android:color="?attr/white" />
<corners android:radius="10dp" />
</shape>
</item>
</ripple>

View File

@ -24,7 +24,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.cardview.widget.CardView
@ -36,7 +35,11 @@
android:id="@+id/settings_profile_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
android:scaleType="centerCrop"
android:foreground="@drawable/rounded_outline"
tools:src="@drawable/profile_bg_orange"
android:contentDescription="@string/account"/>
</androidx.cardview.widget.CardView>
<TextView
@ -50,7 +53,7 @@
android:textColor="?attr/textColor"
android:textSize="18sp"
android:textStyle="normal"
tools:text="Hello world" />
tools:text="Quick Brown Fox" />
</LinearLayout>
<TextView

View File

@ -8,6 +8,8 @@ buildscript {
classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10")
// Universal build config
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1")
}
}
@ -22,6 +24,6 @@ plugins {
id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
//tasks.register<Delete>("clean") {
// delete(rootProject.layout.buildDirectory)
//}

68
library/build.gradle.kts Normal file
View File

@ -0,0 +1,68 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec
plugins {
kotlin("multiplatform")
id("maven-publish")
id("com.android.library")
id("com.codingfeline.buildkonfig")
}
kotlin {
version = "1.0.0"
androidTarget()
jvm()
sourceSets {
commonMain.dependencies {
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
Level 25 or Less. */
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
}
repositories {
mavenLocal()
maven("https://jitpack.io")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildkonfig {
packageName = "com.lagradost.api"
exposeObjectWithName = "BuildConfig"
defaultConfigs {
val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true
buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString())
}
}
android {
compileSdk = 34
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 33
}
// If this is the same com.lagradost.cloudstream3.R stops working
namespace = "com.lagradost.api"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
publishing {
publications {
withType<MavenPublication> {
groupId = "com.lagradost.api"
}
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@ -0,0 +1,21 @@
package com.lagradost.api
import android.util.Log
actual object Log {
actual fun d(tag: String, message: String) {
Log.d(tag, message)
}
actual fun i(tag: String, message: String) {
Log.i(tag, message)
}
actual fun w(tag: String, message: String) {
Log.w(tag, message)
}
actual fun e(tag: String, message: String) {
Log.e(tag, message)
}
}

View File

@ -0,0 +1,8 @@
package com.lagradost.api
expect object Log {
fun d(tag: String, message: String)
fun i(tag: String, message: String)
fun w(tag: String, message: String)
fun e(tag: String, message: String)
}

View File

@ -0,0 +1,3 @@
package com.lagradost.cloudstream3
class ErrorLoadingException(message: String? = null) : Exception(message)

View File

@ -1,10 +1,7 @@
package com.lagradost.cloudstream3.mvvm
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.bumptech.glide.load.HttpException
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.api.BuildConfig
import com.lagradost.api.Log
import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.*
import java.io.InterruptedIOException
@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
}
}
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { it?.let { t -> action(t) } }
}
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
@ -158,14 +143,14 @@ fun<T> throwAbleToResource(
"Connection Timeout\nPlease try again later."
)
}
is HttpException -> {
Resource.Failure(
false,
throwable.statusCode,
null,
throwable.message ?: "HttpException"
)
}
// is HttpException -> {
// Resource.Failure(
// false,
// throwable.statusCode,
// null,
// throwable.message ?: "HttpException"
// )
// }
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}

View File

@ -0,0 +1,19 @@
package com.lagradost.api
actual object Log {
actual fun d(tag: String, message: String) {
println("DEBUG $tag: $message")
}
actual fun i(tag: String, message: String) {
println("INFO $tag: $message")
}
actual fun w(tag: String, message: String) {
println("WARNING $tag: $message")
}
actual fun e(tag: String, message: String) {
println("ERROR $tag: $message")
}
}

View File

@ -1,3 +1,4 @@
rootProject.name = "CloudStream"
include(":app")
include(":app")
include(":library")