staging for sync

This commit is contained in:
LagradOst 2021-12-13 19:41:33 +01:00
parent 04cab02488
commit cc3eba51f3
25 changed files with 504 additions and 1072 deletions

View file

@ -367,7 +367,7 @@ data class AnimeSearchResponse(
override val posterUrl: String?, override val posterUrl: String?,
val year: Int? = null, val year: Int? = null,
val dubStatus: EnumSet<DubStatus>?, val dubStatus: EnumSet<DubStatus>? = null,
val otherName: String? = null, val otherName: String? = null,
val dubEpisodes: Int? = null, val dubEpisodes: Int? = null,
@ -418,7 +418,7 @@ interface LoadResponse {
val plot: String? val plot: String?
val rating: Int? // 0-100 val rating: Int? // 0-100
val tags: List<String>? val tags: List<String>?
val duration: String? var duration: Int? // in minutes
val trailerUrl: String? val trailerUrl: String?
val recommendations: List<SearchResponse>? val recommendations: List<SearchResponse>?
} }
@ -444,29 +444,29 @@ data class AnimeEpisode(
) )
data class TorrentLoadResponse( data class TorrentLoadResponse(
override val name: String, override var name: String,
override val url: String, override var url: String,
override val apiName: String, override var apiName: String,
val magnet: String?, var magnet: String?,
val torrent: String?, var torrent: String?,
override val plot: String?, override var plot: String?,
override val type: TvType = TvType.Torrent, override var type: TvType = TvType.Torrent,
override val posterUrl: String? = null, override var posterUrl: String? = null,
override val year: Int? = null, override var year: Int? = null,
override val rating: Int? = null, override var rating: Int? = null,
override val tags: List<String>? = null, override var tags: List<String>? = null,
override val duration: String? = null, override var duration: Int? = null,
override val trailerUrl: String? = null, override var trailerUrl: String? = null,
override val recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
) : LoadResponse ) : LoadResponse
data class AnimeLoadResponse( data class AnimeLoadResponse(
var engName: String? = null, var engName: String? = null,
var japName: String? = null, var japName: String? = null,
override val name: String, override var name: String,
override val url: String, override var url: String,
override val apiName: String, override var apiName: String,
override val type: TvType, override var type: TvType,
override var posterUrl: String? = null, override var posterUrl: String? = null,
override var year: Int? = null, override var year: Int? = null,
@ -481,7 +481,7 @@ data class AnimeLoadResponse(
var malId: Int? = null, var malId: Int? = null,
var anilistId: Int? = null, var anilistId: Int? = null,
override var rating: Int? = null, override var rating: Int? = null,
override var duration: String? = null, override var duration: Int? = null,
override var trailerUrl: String? = null, override var trailerUrl: String? = null,
override var recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
) : LoadResponse ) : LoadResponse
@ -503,24 +503,52 @@ fun MainAPI.newAnimeLoadResponse(
} }
data class MovieLoadResponse( data class MovieLoadResponse(
override val name: String, override var name: String,
override val url: String, override var url: String,
override val apiName: String, override var apiName: String,
override val type: TvType, override var type: TvType,
val dataUrl: String, var dataUrl: String,
override val posterUrl: String? = null, override var posterUrl: String? = null,
override val year: Int? = null, override var year: Int? = null,
override val plot: String? = null, override var plot: String? = null,
val imdbId: String? = null, var imdbId: String? = null,
override val rating: Int? = null, override var rating: Int? = null,
override val tags: List<String>? = null, override var tags: List<String>? = null,
override val duration: String? = null, override var duration: Int? = null,
override val trailerUrl: String? = null, override var trailerUrl: String? = null,
override val recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
) : LoadResponse ) : LoadResponse
fun MainAPI.newMovieLoadResponse(
name: String,
url: String,
type: TvType,
dataUrl : String,
initializer: MovieLoadResponse.() -> Unit = { }
): MovieLoadResponse {
val builder = MovieLoadResponse(name = name, url = url, apiName = this.name, type = type,dataUrl = dataUrl)
builder.initializer()
return builder
}
fun LoadResponse.setDuration(input : String?) {
if (input == null) return
Regex("([0-9]*)h.*?([0-9]*)m").matchEntire(input)?.groupValues?.let { values ->
if(values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
this.duration = if(minutes != null && hours != null) { hours * 60 + minutes } else null
}
}
Regex("([0-9]*)m").matchEntire(input)?.groupValues?.let { values ->
if(values.size == 2) {
this.duration = values[1].toIntOrNull()
}
}
}
data class TvSeriesEpisode( data class TvSeriesEpisode(
val name: String? = null, val name: String? = null,
val season: Int? = null, val season: Int? = null,
@ -529,25 +557,37 @@ data class TvSeriesEpisode(
val posterUrl: String? = null, val posterUrl: String? = null,
val date: String? = null, val date: String? = null,
val rating: Int? = null, val rating: Int? = null,
val descript: String? = null, val description: String? = null,
) )
data class TvSeriesLoadResponse( data class TvSeriesLoadResponse(
override val name: String, override var name: String,
override val url: String, override var url: String,
override val apiName: String, override var apiName: String,
override val type: TvType, override var type: TvType,
val episodes: List<TvSeriesEpisode>, var episodes: List<TvSeriesEpisode>,
override val posterUrl: String? = null, override var posterUrl: String? = null,
override val year: Int? = null, override var year: Int? = null,
override val plot: String? = null, override var plot: String? = null,
val showStatus: ShowStatus? = null, var showStatus: ShowStatus? = null,
val imdbId: String? = null, var imdbId: String? = null,
override val rating: Int? = null, override var rating: Int? = null,
override val tags: List<String>? = null, override var tags: List<String>? = null,
override val duration: String? = null, override var duration: Int? = null,
override val trailerUrl: String? = null, override var trailerUrl: String? = null,
override val recommendations: List<SearchResponse>? = null, override var recommendations: List<SearchResponse>? = null,
) : LoadResponse ) : LoadResponse
fun MainAPI.newTvSeriesLoadResponse(
name: String,
url: String,
type: TvType,
episodes : List<TvSeriesEpisode>,
initializer: TvSeriesLoadResponse.() -> Unit = { }
): TvSeriesLoadResponse {
val builder = TvSeriesLoadResponse(name = name, url = url, apiName = this.name, type = type, episodes = episodes)
builder.initializer()
return builder
}

View file

@ -370,8 +370,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
showToast(act, act.getString(message), duration) showToast(act, act.getString(message), duration)
} }
fun showToast(act: Activity?, message: String, duration: Int) { fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null) return if (act == null || message == null) return
try { try {
currentToast?.cancel() currentToast?.cancel()
} catch (e: Exception) { } catch (e: Exception) {
@ -390,7 +390,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val toast = Toast(act) val toast = Toast(act)
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.duration = duration toast.duration = duration ?: Toast.LENGTH_SHORT
toast.view = layout toast.view = layout
toast.show() toast.show()
currentToast = toast currentToast = toast

View file

@ -222,6 +222,28 @@ class ZoroProvider : MainAPI() {
) )
} }
val recommendations =
document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item")
.mapNotNull { head ->
val filmPoster = head?.selectFirst(".film-poster")
val epPoster = filmPoster?.selectFirst("img")?.attr("data-src")
val a = head?.selectFirst(".film-detail > .film-name > a")
val epHref = a?.attr("href")
val epTitle = a?.attr("title")
if (epHref == null || epTitle == null || epPoster == null) {
null
} else {
AnimeSearchResponse(
epTitle,
fixUrl(epHref),
this.name,
TvType.Anime,
epPoster,
dubStatus = null
)
}
}
return newAnimeLoadResponse(title, url, TvType.Anime) { return newAnimeLoadResponse(title, url, TvType.Anime) {
japName = japaneseTitle japName = japaneseTitle
engName = title engName = title
@ -231,6 +253,7 @@ class ZoroProvider : MainAPI() {
showStatus = status showStatus = status
plot = description plot = description
this.tags = tags this.tags = tags
this.recommendations = recommendations
} }
} }

View file

@ -2,12 +2,10 @@ package com.lagradost.cloudstream3.metaproviders
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.uwetrottmann.tmdb2.Tmdb import com.uwetrottmann.tmdb2.Tmdb
import com.uwetrottmann.tmdb2.entities.* import com.uwetrottmann.tmdb2.entities.*
import java.util.* import java.util.*
import kotlin.math.roundToInt
/** /**
* episode and season starting from 1 * episode and season starting from 1
@ -34,7 +32,7 @@ open class TmdbProvider : MainAPI() {
// Fuck it, public private api key because github actions won't co-operate. // Fuck it, public private api key because github actions won't co-operate.
// Please no stealy. // Please no stealy.
val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb")
private fun getImageUrl(link: String?): String? { private fun getImageUrl(link: String?): String? {
if (link == null) return null if (link == null) return null
@ -81,38 +79,37 @@ open class TmdbProvider : MainAPI() {
private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { private fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 }
?.mapNotNull { ?.mapNotNull { season ->
it.episodes?.map { season.episodes?.map { episode ->
TvSeriesEpisode( TvSeriesEpisode(
it.name, episode.name,
it.season_number, episode.season_number,
it.episode_number, episode.episode_number,
TmdbLink( TmdbLink(
it.external_ids?.imdb_id ?: this.external_ids?.imdb_id, episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
this.id, this.id,
it.episode_number, episode.episode_number,
it.season_number, episode.season_number,
).toJson(), ).toJson(),
getImageUrl(it.still_path), getImageUrl(episode.still_path),
it.air_date?.toString(), episode.air_date?.toString(),
it.rating, episode.rating,
it.overview, episode.overview,
) )
} ?: (1..(it.episode_count ?: 1)).map { episodeNum -> } ?: (1..(season.episode_count ?: 1)).map { episodeNum ->
TvSeriesEpisode( TvSeriesEpisode(
episode = episodeNum, episode = episodeNum,
data = TmdbLink( data = TmdbLink(
this.external_ids?.imdb_id, this.external_ids?.imdb_id,
this.id, this.id,
episodeNum, episodeNum,
it.season_number, season.season_number,
).toJson(), ).toJson(),
season = it.season_number season = season.season_number
) )
} }
}?.flatten() ?: listOf() }?.flatten() ?: listOf()
// println("STATUS ${this.status}")
return TvSeriesLoadResponse( return TvSeriesLoadResponse(
this.name ?: this.original_name, this.name ?: this.original_name,
getUrl(id, true), getUrl(id, true),
@ -130,7 +127,7 @@ open class TmdbProvider : MainAPI() {
this.external_ids?.imdb_id, this.external_ids?.imdb_id,
this.rating, this.rating,
this.genres?.mapNotNull { it.name }, this.genres?.mapNotNull { it.name },
this.episode_run_time?.average()?.times(60)?.toInt()?.let { secondsToReadable(it, "") }, this.episode_run_time?.average()?.toInt(),
null, null,
this.recommendations?.results?.map { it.toSearchResponse() } this.recommendations?.results?.map { it.toSearchResponse() }
) )
@ -158,7 +155,7 @@ open class TmdbProvider : MainAPI() {
null,//this.status null,//this.status
this.rating, this.rating,
this.genres?.mapNotNull { it.name }, this.genres?.mapNotNull { it.name },
this.runtime?.times(60)?.let { secondsToReadable(it, "") }, this.runtime,
null, null,
this.recommendations?.results?.map { it.toSearchResponse() } this.recommendations?.results?.map { it.toSearchResponse() }
) )

View file

@ -136,19 +136,13 @@ class AllMoviesForYouProvider : MainAPI() {
val data = getLink(document) val data = getLink(document)
?: throw ErrorLoadingException("No Links Found") ?: throw ErrorLoadingException("No Links Found")
return MovieLoadResponse( return newMovieLoadResponse(title,url,type,mapper.writeValueAsString(data.filter { it != "about:blank" })) {
title, posterUrl = backgroundPoster
url, this.year = year?.toIntOrNull()
this.name, this.plot = descipt
type, this.rating = rating
mapper.writeValueAsString(data.filter { it != "about:blank" }), setDuration(duration)
backgroundPoster, }
year?.toIntOrNull(),
descipt,
null,
rating,
duration = duration
)
} }
} }

View file

@ -162,22 +162,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/") val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/")
return MovieLoadResponse( return newMovieLoadResponse(title, url, TvType.Movie, webViewUrl) {
title, this.year = year
url, this.posterUrl = posterUrl
this.name, this.plot = plot
TvType.Movie, setDuration(duration)
webViewUrl, }
posterUrl,
year,
plot,
null,
null,
null,
duration,
null,
null
)
} else { } else {
val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text
val seasonsDocument = Jsoup.parse(seasonsHtml) val seasonsDocument = Jsoup.parse(seasonsHtml)
@ -212,23 +202,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
} }
} }
return TvSeriesLoadResponse( return newTvSeriesLoadResponse(title,url,TvType.TvSeries,episodes) {
title, this.posterUrl = posterUrl
url, this.year = year
this.name, this.plot = plot
TvType.TvSeries, setDuration(duration)
episodes, }
posterUrl,
year,
plot,
null,
null,
null,
null,
duration,
null,
null
)
} }
} }

View file

@ -1,482 +0,0 @@
package com.lagradost.cloudstream3.movieproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.util.concurrent.TimeUnit
class ThenosProvider : MainAPI() {
override val mainUrl = "https://www.thenos.org"
override val name = "Thenos"
override val hasQuickSearch = true
override val hasMainPage = true
override val hasChromecastSupport = false
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
)
override fun getMainPage(): HomePageResponse {
val map = mapOf(
"New Releases" to "released",
"Recently Added in Movies" to "recent",
"Recently Added in Shows" to "recent/shows",
"Top Rated" to "rating"
)
val list = ArrayList<HomePageList>()
map.entries.forEach {
val url = "$apiUrl/library/${it.value}"
val response = app.get(url).text
val mapped = mapper.readValue<ThenosLoadResponse>(response)
mapped.Metadata?.mapNotNull { meta ->
meta?.toSearchResponse()
}?.let { searchResponses ->
list.add(
HomePageList(
it.key,
searchResponses
)
)
}
}
return HomePageResponse(
list
)
}
private fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
private val apiUrl = "https://api.thenos.org"
override fun quickSearch(query: String): List<SearchResponse> {
val url = "$apiUrl/library/search?query=$query"
return searchFromUrl(url)
}
data class ThenosSearchResponse(
@JsonProperty("size") val size: Int?,
@JsonProperty("Hub") val Hub: List<Hub>?
)
data class Part(
@JsonProperty("id") val id: Long?,
@JsonProperty("key") val key: String?,
@JsonProperty("duration") val duration: Long?,
@JsonProperty("file") val file: String?,
@JsonProperty("size") val size: Long?,
@JsonProperty("audioProfile") val audioProfile: String?,
@JsonProperty("container") val container: String?,
@JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?,
@JsonProperty("optimizedForStreaming") val optimizedForStreaming: Boolean?,
@JsonProperty("videoProfile") val videoProfile: String?
)
data class Media(
@JsonProperty("id") val id: Long?,
@JsonProperty("duration") val duration: Long?,
@JsonProperty("bitrate") val bitrate: Long?,
@JsonProperty("width") val width: Long?,
@JsonProperty("height") val height: Long?,
@JsonProperty("aspectRatio") val aspectRatio: Double?,
@JsonProperty("audioChannels") val audioChannels: Long?,
@JsonProperty("audioCodec") val audioCodec: String?,
@JsonProperty("videoCodec") val videoCodec: String?,
@JsonProperty("videoResolution") val videoResolution: String?,
@JsonProperty("container") val container: String?,
@JsonProperty("videoFrameRate") val videoFrameRate: String?,
@JsonProperty("optimizedForStreaming") val optimizedForStreaming: Long?,
@JsonProperty("audioProfile") val audioProfile: String?,
@JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?,
@JsonProperty("videoProfile") val videoProfile: String?,
@JsonProperty("Part") val Part: List<Part>?
)
data class Genre(
@JsonProperty("tag") val tag: String?
)
data class Country(
@JsonProperty("tag") val tag: String?
)
data class Role(
@JsonProperty("tag") val tag: String?
)
data class Hub(
@JsonProperty("title") val title: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("hubIdentifier") val hubIdentifier: String?,
@JsonProperty("context") val context: String?,
@JsonProperty("size") val size: Int?,
@JsonProperty("more") val more: Boolean?,
@JsonProperty("style") val style: String?,
@JsonProperty("Metadata") val Metadata: List<Metadata>?
)
data class Metadata(
@JsonProperty("librarySectionTitle") val librarySectionTitle: String?,
@JsonProperty("ratingKey") val ratingKey: String?,
@JsonProperty("key") val key: String?,
@JsonProperty("guid") val guid: String?,
@JsonProperty("studio") val studio: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("librarySectionID") val librarySectionID: Int?,
@JsonProperty("librarySectionKey") val librarySectionKey: String?,
@JsonProperty("contentRating") val contentRating: String?,
@JsonProperty("summary") val summary: String?,
@JsonProperty("audienceRating") val audienceRating: Int?,
@JsonProperty("year") val year: Int?,
@JsonProperty("thumb") val thumb: String?,
@JsonProperty("art") val art: String?,
@JsonProperty("duration") val duration: Int?,
@JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?,
@JsonProperty("addedAt") val addedAt: Int?,
@JsonProperty("updatedAt") val updatedAt: Int?,
@JsonProperty("audienceRatingImage") val audienceRatingImage: String?,
@JsonProperty("Media") val Media: List<Media>?,
@JsonProperty("Genre") val Genre: List<Genre>?,
@JsonProperty("Director") val Director: List<Director>?,
@JsonProperty("Country") val Country: List<Country>?,
@JsonProperty("Role") val Role: List<Role>?
)
data class Director(
@JsonProperty("tag") val tag: String
)
private fun Metadata.toSearchResponse(): SearchResponse? {
if (type == "movie") {
return MovieSearchResponse(
title ?: "",
ratingKey ?: return null,
this@ThenosProvider.name,
TvType.Movie,
art?.let { "$apiUrl$it" },
year
)
} else if (type == "show") {
return TvSeriesSearchResponse(
title ?: "",
ratingKey ?: return null,
this@ThenosProvider.name,
TvType.TvSeries,
art?.let { "$apiUrl$it" },
year,
null
)
}
return null
}
private fun searchFromUrl(url: String): List<SearchResponse> {
val response = app.get(url).text
val test = mapper.readValue<ThenosSearchResponse>(response)
val returnValue = ArrayList<SearchResponse>()
test.Hub?.forEach {
it.Metadata?.forEach metadata@{ meta ->
if (meta.ratingKey == null || meta.title == null) return@metadata
meta.toSearchResponse()?.let { response -> returnValue.add(response) }
}
}
return returnValue
}
override fun search(query: String): List<SearchResponse> {
val url = "$apiUrl/library/search/advance?query=$query"
return searchFromUrl(url)
}
data class ThenosSource(
@JsonProperty("title") val title: String?,
@JsonProperty("image") val image: String?,
@JsonProperty("sources") val sources: List<Sources>?,
@JsonProperty("tracks") val tracks: List<Tracks>
)
data class Sources(
@JsonProperty("file") val file: String?,
@JsonProperty("label") val label: String?,
@JsonProperty("default") val default: Boolean?,
@JsonProperty("type") val type: String?
)
data class Tracks(
@JsonProperty("file") val file: String?,
@JsonProperty("label") val label: String?,
@JsonProperty("kind") val kind: String?
)
override fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val url = "$apiUrl/library/watch/$data"
val response = app.get(url).text
val mapped = mapper.readValue<ThenosSource>(response)
mapped.sources?.forEach { source ->
val isM3u8 = source.type != "video/mp4"
val token = app.get("https://token.noss.workers.dev/").text
val authorization =
base64Decode(token)
callback.invoke(
ExtractorLink(
this.name,
"${this.name} ${source.label ?: ""}",
(source.file)?.split("/")?.lastOrNull()?.let {
"https://www.googleapis.com/drive/v3/files/$it?alt=media"
} ?: return@forEach,
"https://www.thenos.org/",
getQualityFromName(source.label ?: ""),
isM3u8,
mapOf("authorization" to "Bearer $authorization")
)
)
}
mapped.tracks.forEach {
subtitleCallback.invoke(
SubtitleFile(
it.label ?: "English",
it.file ?: return@forEach
)
)
}
return true
}
data class ThenosLoadResponse(
@JsonProperty("size") val size: Long?,
@JsonProperty("allowSync") val allowSync: Boolean?,
@JsonProperty("augmentationKey") val augmentationKey: String?,
@JsonProperty("identifier") val identifier: String?,
@JsonProperty("librarySectionID") val librarySectionID: Long?,
@JsonProperty("librarySectionTitle") val librarySectionTitle: String?,
@JsonProperty("librarySectionUUID") val librarySectionUUID: String?,
@JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?,
@JsonProperty("mediaTagVersion") val mediaTagVersion: Long?,
@JsonProperty("Metadata") val Metadata: List<Metadata?>?
)
data class ThenosSeriesResponse(
@JsonProperty("size") val size: Long?,
@JsonProperty("allowSync") val allowSync: Boolean?,
@JsonProperty("art") val art: String?,
@JsonProperty("identifier") val identifier: String?,
@JsonProperty("key") val key: String?,
@JsonProperty("librarySectionID") val librarySectionID: Long?,
@JsonProperty("librarySectionTitle") val librarySectionTitle: String?,
@JsonProperty("librarySectionUUID") val librarySectionUUID: String?,
@JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?,
@JsonProperty("mediaTagVersion") val mediaTagVersion: Long?,
@JsonProperty("nocache") val nocache: Boolean?,
@JsonProperty("parentIndex") val parentIndex: Long?,
@JsonProperty("parentTitle") val parentTitle: String?,
@JsonProperty("parentYear") val parentYear: Long?,
@JsonProperty("summary") val summary: String?,
@JsonProperty("theme") val theme: String?,
@JsonProperty("thumb") val thumb: String?,
@JsonProperty("title1") val title1: String?,
@JsonProperty("title2") val title2: String?,
@JsonProperty("viewGroup") val viewGroup: String?,
@JsonProperty("viewMode") val viewMode: Long?,
@JsonProperty("Metadata") val Metadata: List<SeriesMetadata>?
)
data class SeriesMetadata(
@JsonProperty("ratingKey") val ratingKey: String?,
@JsonProperty("key") val key: String?,
@JsonProperty("parentRatingKey") val parentRatingKey: String?,
@JsonProperty("guid") val guid: String?,
@JsonProperty("parentGuid") val parentGuid: String?,
@JsonProperty("parentStudio") val parentStudio: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("parentKey") val parentKey: String?,
@JsonProperty("parentTitle") val parentTitle: String?,
@JsonProperty("summary") val summary: String?,
@JsonProperty("index") val index: Long?,
@JsonProperty("parentIndex") val parentIndex: Long?,
@JsonProperty("parentYear") val parentYear: Long?,
@JsonProperty("thumb") val thumb: String?,
@JsonProperty("art") val art: String?,
@JsonProperty("parentThumb") val parentThumb: String?,
@JsonProperty("parentTheme") val parentTheme: String?,
@JsonProperty("leafCount") val leafCount: Long?,
@JsonProperty("viewedLeafCount") val viewedLeafCount: Long?,
@JsonProperty("addedAt") val addedAt: Long?,
@JsonProperty("updatedAt") val updatedAt: Int?
)
data class SeasonResponse(
@JsonProperty("size") val size: Long?,
@JsonProperty("allowSync") val allowSync: Boolean?,
@JsonProperty("art") val art: String?,
@JsonProperty("grandparentContentRating") val grandparentContentRating: String?,
@JsonProperty("grandparentRatingKey") val grandparentRatingKey: Long?,
@JsonProperty("grandparentStudio") val grandparentStudio: String?,
@JsonProperty("grandparentTheme") val grandparentTheme: String?,
@JsonProperty("grandparentThumb") val grandparentThumb: String?,
@JsonProperty("grandparentTitle") val grandparentTitle: String?,
@JsonProperty("identifier") val identifier: String?,
@JsonProperty("key") val key: String?,
@JsonProperty("librarySectionID") val librarySectionID: Long?,
@JsonProperty("librarySectionTitle") val librarySectionTitle: String?,
@JsonProperty("librarySectionUUID") val librarySectionUUID: String?,
@JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?,
@JsonProperty("mediaTagVersion") val mediaTagVersion: Long?,
@JsonProperty("nocache") val nocache: Boolean?,
@JsonProperty("parentIndex") val parentIndex: Long?,
@JsonProperty("parentTitle") val parentTitle: String?,
@JsonProperty("summary") val summary: String?,
@JsonProperty("theme") val theme: String?,
@JsonProperty("thumb") val thumb: String?,
@JsonProperty("title1") val title1: String?,
@JsonProperty("title2") val title2: String?,
@JsonProperty("viewGroup") val viewGroup: String?,
@JsonProperty("viewMode") val viewMode: Long?,
@JsonProperty("Metadata") val Metadata: List<SeasonMetadata>?
)
data class SeasonMetadata(
@JsonProperty("ratingKey") val ratingKey: String?,
@JsonProperty("key") val key: String?,
@JsonProperty("parentRatingKey") val parentRatingKey: String?,
@JsonProperty("grandparentRatingKey") val grandparentRatingKey: String?,
@JsonProperty("guid") val guid: String?,
@JsonProperty("parentGuid") val parentGuid: String?,
@JsonProperty("grandparentGuid") val grandparentGuid: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("grandparentKey") val grandparentKey: String?,
@JsonProperty("parentKey") val parentKey: String?,
@JsonProperty("grandparentTitle") val grandparentTitle: String?,
@JsonProperty("parentTitle") val parentTitle: String?,
@JsonProperty("contentRating") val contentRating: String?,
@JsonProperty("summary") val summary: String?,
@JsonProperty("index") val index: Int?,
@JsonProperty("parentIndex") val parentIndex: Int?,
@JsonProperty("audienceRating") val audienceRating: Double?,
@JsonProperty("thumb") val thumb: String?,
@JsonProperty("art") val art: String?,
@JsonProperty("parentThumb") val parentThumb: String?,
@JsonProperty("grandparentThumb") val grandparentThumb: String?,
@JsonProperty("grandparentArt") val grandparentArt: String?,
@JsonProperty("grandparentTheme") val grandparentTheme: String?,
@JsonProperty("duration") val duration: Long?,
@JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?,
@JsonProperty("addedAt") val addedAt: Long?,
@JsonProperty("updatedAt") val updatedAt: Long?,
@JsonProperty("audienceRatingImage") val audienceRatingImage: String?,
@JsonProperty("Media") val Media: List<Media>?,
@JsonProperty("Director") val Director: List<Director>?,
@JsonProperty("Role") val Role: List<Role>?
)
private fun getAllEpisodes(id: String): List<TvSeriesEpisode> {
val episodes = ArrayList<TvSeriesEpisode>()
val url = "$apiUrl/library/metadata/$id/children"
val response = app.get(url).text
val mapped = mapper.readValue<ThenosSeriesResponse>(response)
mapped.Metadata?.forEach { series_meta ->
val fixedUrl = apiUrl + series_meta.key
val child = app.get(fixedUrl).text
val mappedSeason = mapper.readValue<SeasonResponse>(child)
mappedSeason.Metadata?.forEach mappedSeason@{ meta ->
episodes.add(
TvSeriesEpisode(
meta.title,
meta.parentIndex,
meta.index,
meta.ratingKey ?: return@mappedSeason,
meta.thumb?.let { "$apiUrl$it" },
meta.originallyAvailableAt,
(meta.audienceRating?.times(10))?.toInt(),
meta.summary
)
)
}
}
return episodes
}
override fun load(url: String): LoadResponse? {
val fixedUrl = "$apiUrl/library/metadata/${url.split("/").last()}"
val response = app.get(fixedUrl).text
val mapped = mapper.readValue<ThenosLoadResponse>(response)
val isShow = mapped.Metadata?.any { it?.type == "show" } == true
val metadata = mapped.Metadata?.getOrNull(0) ?: return null
return if (!isShow) {
MovieLoadResponse(
metadata.title ?: "No title found",
"$mainUrl/movie/${metadata.ratingKey}",
this.name,
TvType.Movie,
metadata.ratingKey ?: return null,
metadata.art?.let { "$apiUrl$it" },
metadata.year,
metadata.summary,
null, // with Guid this is possible
metadata.audienceRating?.times(10),
metadata.Genre?.mapNotNull { it.tag },
metadata.duration?.let { secondsToReadable(it / 1000, "") },
null
)
} else {
TvSeriesLoadResponse(
metadata.title ?: "No title found",
"$mainUrl/show/${metadata.ratingKey}",
this.name,
TvType.TvSeries,
metadata.ratingKey?.let { getAllEpisodes(it) } ?: return null,
metadata.art?.let { "$apiUrl$it" },
metadata.year,
metadata.summary,
null, // with Guid this is possible
null,// with Guid this is possible
metadata.audienceRating?.times(10),
metadata.Genre?.mapNotNull { it.tag },
metadata.duration?.let { secondsToReadable(it / 1000, "") },
null
)
}
}
}

View file

@ -1,305 +0,0 @@
package com.lagradost.cloudstream3.movieproviders
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper
import org.jsoup.Jsoup
// referer = https://trailers.to, USERAGENT ALSO REQUIRED
class TrailersToProvider : MainAPI() {
override val mainUrl = "https://trailers.to"
override val name = "Trailers.to"
override val hasQuickSearch = true
override val hasMainPage = true
override val hasChromecastSupport = false
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
)
override val vpnStatus = VPNStatus.MightBeNeeded
override fun getMainPage(): HomePageResponse? {
val response = app.get(mainUrl).text
val document = Jsoup.parse(response)
val returnList = ArrayList<HomePageList>()
val docs = document.select("section.section > div.container")
for (doc in docs) {
val epList = doc.selectFirst("> div.owl-carousel") ?: continue
val title = doc.selectFirst("> div.text-center > h2").text()
val list = epList.select("> div.item > div.box-nina")
val isMovieType = title.contains("Movie")
val currentList = list.mapNotNull { head ->
val hrefItem = head.selectFirst("> div.box-nina-media > a")
val href = fixUrl(hrefItem.attr("href"))
val img = hrefItem.selectFirst("> img")
val posterUrl = img.attr("src")
val name = img.attr("alt")
return@mapNotNull if (isMovieType) MovieSearchResponse(
name,
href,
this.name,
TvType.Movie,
posterUrl,
null
) else TvSeriesSearchResponse(
name,
href,
this.name,
TvType.TvSeries,
posterUrl,
null, null
)
}
if (currentList.isNotEmpty()) {
returnList.add(HomePageList(title, currentList))
}
}
if (returnList.size <= 0) return null
return HomePageResponse(returnList)
//section.section > div.container > div.owl-carousel
}
override fun quickSearch(query: String): List<SearchResponse> {
val url = "$mainUrl/en/quick-search?q=$query"
val response = app.get(url).text
val document = Jsoup.parse(response)
val items = document.select("div.group-post-minimal > a.post-minimal")
if (items.isNullOrEmpty()) return ArrayList()
val returnValue = ArrayList<SearchResponse>()
for (item in items) {
val href = fixUrl(item.attr("href"))
val poster = item.selectFirst("> div.post-minimal-media > img").attr("src")
val header = item.selectFirst("> div.post-minimal-main")
val name = header.selectFirst("> span.link-black").text()
val info = header.select("> p")
val year = info?.get(1)?.text()?.toIntOrNull()
val isTvShow = href.contains("/tvshow/")
returnValue.add(
if (isTvShow) {
TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null)
} else {
MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year)
}
)
}
return returnValue
}
override fun search(query: String): List<SearchResponse> {
val url = "$mainUrl/en/popular/movies-tvshows-collections?q=$query"
val response = app.get(url).text
val document = Jsoup.parse(response)
val items = document.select("div.col-lg-8 > article.list-item")
if (items.isNullOrEmpty()) return ArrayList()
val returnValue = ArrayList<SearchResponse>()
for (item in items) {
val poster = item.selectFirst("> div.tour-modern-media > a.tour-modern-figure > img").attr("src")
val infoDiv = item.selectFirst("> div.tour-modern-main")
val nameHeader = infoDiv.select("> h5.tour-modern-title > a").last()
val name = nameHeader.text()
val href = fixUrl(nameHeader.attr("href"))
val year = infoDiv.selectFirst("> div > span.small-text")?.text()?.takeLast(4)?.toIntOrNull()
val isTvShow = href.contains("/tvshow/")
returnValue.add(
if (isTvShow) {
TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null)
} else {
MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year)
}
)
}
return returnValue
}
private fun loadLink(
data: String,
callback: (ExtractorLink) -> Unit,
): Boolean {
val response = app.get(data).text
val url = "<source src='(.*?)'".toRegex().find(response)?.groupValues?.get(1)
if (url != null) {
callback.invoke(ExtractorLink(this.name, this.name, url, mainUrl, Qualities.Unknown.value, false))
}
return url != null
}
private fun loadSubs(url: String, subtitleCallback: (SubtitleFile) -> Unit) {
if (url.isEmpty()) return
val response = app.get(fixUrl(url)).text
val document = Jsoup.parse(response)
val items = document.select("div.list-group > a.list-group-item")
for (item in items) {
val hash = item.attr("hash") ?: continue
val languageCode = item.attr("languagecode") ?: continue
if (hash.isEmpty()) continue
if (languageCode.isEmpty()) continue
subtitleCallback.invoke(
SubtitleFile(
SubtitleHelper.fromTwoLettersToLanguage(languageCode) ?: languageCode,
"$mainUrl/subtitles/$hash"
)
)
}
}
override fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
if (isCasting) return false
val pairData = mapper.readValue<Pair<String, String>>(data)
val url = pairData.second
val isMovie = url.contains("/web-sources/")
if (isMovie) {
val isSucc = loadLink(url, callback)
val subUrl = pairData.first
loadSubs(subUrl, subtitleCallback)
return isSucc
} else if (url.contains("/episode/")) {
val response = app.get(url, params = mapOf("preview" to "1")).text
val document = Jsoup.parse(response)
// val qSub = document.select("subtitle-content")
val subUrl = document.select("subtitle-content")?.attr("data-url") ?: ""
val subData = fixUrl(document.selectFirst("content").attr("data-url") ?: return false)
val isSucc = if (subData.contains("/web-sources/")) {
loadLink(subData, callback)
} else false
loadSubs(subUrl, subtitleCallback)
return isSucc
}
return false
}
override fun load(url: String): LoadResponse {
val response = app.get(if (url.endsWith("?preview=1")) url else "$url?preview=1").text
val document = Jsoup.parse(response)
var title = document?.selectFirst("h2.breadcrumbs-custom-title > a")?.text()
?: throw ErrorLoadingException("Service might be unavailable")
val metaInfo = document.select("div.post-info-meta > ul.post-info-meta-list > li")
val year = metaInfo?.get(0)?.selectFirst("> span.small-text")?.text()?.takeLast(4)?.toIntOrNull()
val rating = parseRating(metaInfo?.get(1)?.selectFirst("> span.small-text")?.text()?.replace("/ 10", ""))
val duration = metaInfo?.get(2)?.selectFirst("> span.small-text")?.text()
val imdbUrl = metaInfo?.get(3)?.selectFirst("> a")?.attr("href")
val trailer = metaInfo?.get(4)?.selectFirst("> a")?.attr("href")
val poster = document.selectFirst("div.slider-image > a > img").attr("src")
val descriptHeader = document.selectFirst("article.post-info")
title = title.substring(0, title.length - 6) // REMOVE YEAR
val descript = descriptHeader.select("> div > p").text()
val table = descriptHeader.select("> table.post-info-table > tbody > tr > td")
var generes: List<String>? = null
for (i in 0 until table.size / 2) {
val header = table[i * 2].text()
val info = table[i * 2 + 1]
when (header) {
"Genre" -> generes = info.text().split(",")
}
}
val tags = if (generes == null) null else ArrayList(generes)
val isTvShow = url.contains("/tvshow/")
if (isTvShow) {
val episodes = document.select("#seasons-accordion .card-body > .tour-modern")
?: throw ErrorLoadingException("No Episodes found")
val parsedEpisodes = episodes.withIndex().map { (index, item) ->
val epPoster = item.selectFirst("img").attr("src")
val main = item.selectFirst(".tour-modern-main")
val titleHeader = main.selectFirst("a")
val titleName = titleHeader.text()
val href = fixUrl(titleHeader.attr("href"))
val gValues =
Regex(""".*?[\w\s]+ ([0-9]+)(?::[\w\s]+)?\s-\s(?:Episode )?([0-9]+)?(?:: )?(.*)""").find(titleName)?.destructured
val season = gValues?.component1()?.toIntOrNull()
var episode = gValues?.component2()?.toIntOrNull()
if (episode == null) {
episode = index + 1
}
val epName =
if (gValues?.component3()?.isNotEmpty() == true) gValues.component3() else "Episode $episode"
val infoHeaders = main.select("span.small-text")
val date = infoHeaders?.get(0)?.text()
val ratingText = infoHeaders?.get(1)?.text()?.replace("/ 10", "")
val epRating = if (ratingText == null) null else parseRating(ratingText)
val epDescript = main.selectFirst("p")?.text()
TvSeriesEpisode(
epName,
season,
episode,
mapper.writeValueAsString(Pair("", href)),
epPoster,
date,
epRating,
epDescript
)
}
return TvSeriesLoadResponse(
title,
url,
this.name,
TvType.TvSeries,
ArrayList(parsedEpisodes),
poster,
year,
descript,
null,
imdbUrlToIdNullable(imdbUrl),
rating,
tags,
duration,
trailer
)
} else {
//https://trailers.to/en/subtitle-details/2086212/jungle-cruise-2021?imdbId=tt0870154&season=0&episode=0
//https://trailers.to/en/movie/2086212/jungle-cruise-2021
val subUrl = if (imdbUrl != null) {
val imdbId = imdbUrlToId(imdbUrl)
url.replace("/movie/", "/subtitle-details/") + "?imdbId=$imdbId&season=0&episode=0"
} else ""
val data = mapper.writeValueAsString(
Pair(
subUrl,
fixUrl(
document.selectFirst("content")?.attr("data-url")
?: throw ErrorLoadingException("Link not found")
)
)
)
return MovieLoadResponse(
title,
url,
this.name,
TvType.Movie,
data,
poster,
year,
descript,
imdbUrlToIdNullable(imdbUrl),
rating,
tags,
duration,
trailer
)
}
}
}

View file

@ -44,11 +44,11 @@ class VfSerieProvider : MainAPI() {
private fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example private fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example
val response = app.get(original).text val response = app.get(original).text
val url = "iframe .*src=\\\"(.*?)\\\"".toRegex().find(response)?.groupValues?.get(1) val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1)
.toString() // https://vudeo.net/embed-7jdb1t5b2mvo.html for example .toString() // https://vudeo.net/embed-7jdb1t5b2mvo.html for example
val vudoResponse = app.get(url).text val vudoResponse = app.get(url).text
val document = Jsoup.parse(vudoResponse) val document = Jsoup.parse(vudoResponse)
return Regex("sources: \\[\"(.*?)\"\\]").find(document.html())?.groupValues?.get(1) return Regex("sources: \\[\"(.*?)\"]").find(document.html())?.groupValues?.get(1)
.toString() // direct mp4 link, https://m5.vudeo.net/2vp3xgpw2avjdohilpfbtyuxzzrqzuh4z5yxvztral5k3rjnba6f4byj3saa/v.mp4 for exemple .toString() // direct mp4 link, https://m5.vudeo.net/2vp3xgpw2avjdohilpfbtyuxzzrqzuh4z5yxvztral5k3rjnba6f4byj3saa/v.mp4 for exemple
} }

View file

@ -6,12 +6,6 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
abstract class AccountManager(private val defIndex: Int) : OAuth2API { abstract class AccountManager(private val defIndex: Int) : OAuth2API {
// don't change this as all keys depend on it
open val idPrefix: String
get() {
throw(NotImplementedError())
}
var accountIndex = defIndex var accountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex" protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active" private val accountActiveKey get() = "${idPrefix}_active"

View file

@ -10,6 +10,9 @@ interface OAuth2API {
val name: String val name: String
val redirectUrl: String val redirectUrl: String
// don't change this as all keys depend on it
val idPrefix : String
fun handleRedirect(context: Context, url: String) fun handleRedirect(context: Context, url: String)
fun authenticate(context: Context) fun authenticate(context: Context)

View file

@ -5,11 +5,10 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync //TODO dropbox sync
class Dropbox : OAuth2API { class Dropbox : OAuth2API {
override val idPrefix = "dropbox"
override val name = "Dropbox" override val name = "Dropbox"
override val key: String override val key = "zlqsamadlwydvb2"
get() = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin"
override val redirectUrl: String
get() = "dropboxlogin"
override fun authenticate(context: Context) { override fun authenticate(context: Context) {
TODO("Not yet implemented") TODO("Not yet implemented")

View file

@ -48,12 +48,11 @@ import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import java.util.* import java.util.*
@ -124,15 +123,8 @@ class HomeFragment : Fragment() {
} }
private fun fixGrid() { private fun fixGrid() {
val compactView = activity?.getGridIsCompact() ?: false activity?.getSpanCount()?.let {
val spanCountLandscape = if (compactView) 2 else 6 currentSpan = it
val spanCountPortrait = if (compactView) 1 else 3
val orientation = resources.configuration.orientation
currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCountLandscape
} else {
spanCountPortrait
} }
configEvent.invoke(currentSpan) configEvent.invoke(currentSpan)
} }

View file

@ -19,11 +19,8 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
@ -34,12 +31,22 @@ import java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
companion object { companion object {
fun push(activity: Activity?, mainApi: Boolean = true, autoSearch: String? = null) { fun pushSearch(activity: Activity?, autoSearch: String? = null) {
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", mainApi) putBoolean("mainapi", true)
putString("autosearch", autoSearch) putString("autosearch", autoSearch)
}) })
} }
fun pushSync(activity: Activity?, autoSearch: String? = null, callback : (SearchClickCallback) -> Unit) {
clickCallback = callback
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", false)
putString("autosearch", autoSearch)
})
}
var clickCallback : ((SearchClickCallback) -> Unit)? = null
} }
private val searchViewModel: SearchViewModel by activityViewModels() private val searchViewModel: SearchViewModel by activityViewModels()
@ -56,6 +63,11 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
return inflater.inflate(R.layout.quick_search, container, false) return inflater.inflate(R.layout.quick_search, container, false)
} }
override fun onDestroy() {
super.onDestroy()
clickCallback = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root) context?.fixPaddingStatusbar(quick_search_root)
@ -96,7 +108,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(activity, callback)
} else { } else {
//TODO MAL RESPONSE clickCallback?.invoke(callback)
} }
} }
else -> SearchHelper.handleSearchClickCallback(activity, callback) else -> SearchHelper.handleSearchClickCallback(activity, callback)
@ -135,7 +147,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
is Resource.Success -> { is Resource.Success -> {
it.value.let { data -> it.value.let { data ->
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
(cardSpace?.adapter as SearchAdapter?)?.apply { (search_autofit_results?.adapter as SearchAdapter?)?.apply {
cardList = data.toList() cardList = data.toList()
notifyDataSetChanged() notifyDataSetChanged()
} }

View file

@ -8,6 +8,7 @@ import android.content.Context.CLIPBOARD_SERVICE
import android.content.Intent import android.content.Intent
import android.content.Intent.* import android.content.Intent.*
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -15,7 +16,9 @@ import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.isGone import androidx.core.view.isGone
@ -39,6 +42,8 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -47,6 +52,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerData
import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.ui.player.PlayerFragment
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
@ -57,12 +64,15 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
@ -256,7 +266,65 @@ class ResultFragment : Fragment() {
} }
var startAction: Int? = null var startAction: Int? = null
var startValue: Int? = null private var startValue: Int? = null
private fun updateSync(id: Int) {
val syncList = context?.getSync(id, SyncApis.map { it.idPrefix }) ?: return
val list = ArrayList<Pair<SyncAPI, String>>()
for (i in 0 until SyncApis.count()) {
val res = syncList[i] ?: continue
list.add(Pair(SyncApis[i], res))
}
viewModel.updateSync(context, list)
}
private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) {
if (arg == null) {
textView?.isVisible = false
} else {
val text = context?.getString(format)?.format(arg)
if (text == null) {
textView?.isVisible = false
} else {
textView?.isVisible = true
textView?.text = text
}
}
}
private fun setDuration(duration: Int?) {
setFormatText(result_meta_duration, R.string.duration_format, duration)
}
private fun setYear(year: Int?) {
setFormatText(result_meta_year, R.string.year_format, year)
}
private fun setRating(rating: Int?) {
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000))
}
private fun setRecommendations(rec: List<SearchResponse>?) {
return
result_recommendations?.isGone = rec.isNullOrEmpty()
rec?.let { list ->
(result_recommendations?.adapter as SearchAdapter?)?.apply {
cardList = list
notifyDataSetChanged()
}
}
}
private fun fixGrid() {
activity?.getSpanCount()?.let { count ->
result_recommendations?.spanCount = count
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
fixGrid()
}
private fun lateFixDownloadButton(show: Boolean) { private fun lateFixDownloadButton(show: Boolean) {
if (!show || currentType?.isMovieType() == false) { if (!show || currentType?.isMovieType() == false) {
@ -273,6 +341,7 @@ class ResultFragment : Fragment() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fixGrid()
val restart = arguments?.getBoolean("restart") ?: false val restart = arguments?.getBoolean("restart") ?: false
if (restart) { if (restart) {
@ -949,6 +1018,20 @@ class ResultFragment : Fragment() {
currentId = it currentId = it
} }
observe(viewModel.sync) { sync ->
for (s in sync) {
when (s) {
is Resource.Success -> {
val d = s.value ?: continue
setDuration(d.duration)
setRating(d.publicScore)
}
else -> {
}
}
}
}
observe(viewModel.resultResponse) { data -> observe(viewModel.resultResponse) { data ->
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
@ -965,7 +1048,7 @@ class ResultFragment : Fragment() {
VPNStatus.Torrent -> getString(R.string.vpn_torrent) VPNStatus.Torrent -> getString(R.string.vpn_torrent)
else -> "" else -> ""
} }
result_vpn?.visibility = if (api.vpnStatus == VPNStatus.None) GONE else VISIBLE result_vpn?.isGone = api.vpnStatus == VPNStatus.None
result_info?.text = when (api.providerType) { result_info?.text = when (api.providerType) {
ProviderType.MetaProvider -> getString(R.string.provider_info_meta) ProviderType.MetaProvider -> getString(R.string.provider_info_meta)
@ -973,8 +1056,6 @@ class ResultFragment : Fragment() {
} }
result_info?.isVisible = api.providerType == ProviderType.MetaProvider result_info?.isVisible = api.providerType == ProviderType.MetaProvider
//result_bookmark_button.text = getString(R.string.type_watching)
currentHeaderName = d.name currentHeaderName = d.name
currentType = d.type currentType = d.type
@ -992,7 +1073,7 @@ class ResultFragment : Fragment() {
} }
result_search?.setOnClickListener { result_search?.setOnClickListener {
QuickSearchFragment.push(activity, true, d.name) QuickSearchFragment.pushSearch(activity, d.name)
} }
result_share?.setOnClickListener { result_share?.setOnClickListener {
@ -1003,6 +1084,20 @@ class ResultFragment : Fragment() {
startActivity(createChooser(i, d.name)) startActivity(createChooser(i, d.name))
} }
updateSync(d.getId())
result_add_sync?.setOnClickListener {
QuickSearchFragment.pushSync(activity, d.name) { click ->
context?.addSync(d.getId(), click.card.apiName, click.card.url)?.let {
showToast(
activity,
context?.getString(R.string.added_sync_format)?.format(click.card.name),
Toast.LENGTH_SHORT
)
}
updateSync(d.getId())
}
}
val metadataInfoArray = ArrayList<Pair<Int, String>>() val metadataInfoArray = ArrayList<Pair<Int, String>>()
if (d is AnimeLoadResponse) { if (d is AnimeLoadResponse) {
val status = when (d.showStatus) { val status = when (d.showStatus) {
@ -1015,23 +1110,10 @@ class ResultFragment : Fragment() {
} }
} }
result_meta_year?.isGone = d.year == null setDuration(d.duration)
result_meta_year?.text = d.year?.toString() ?: "" setYear(d.year)
if (d.rating == null) { setRating(d.rating)
result_meta_rating?.isVisible = false setRecommendations(d.recommendations)
} else {
result_meta_rating?.isVisible = true
result_meta_rating?.text = "%.1f/10.0".format(d.rating!!.toFloat() / 10f).replace(",", ".")
}
val duration = d.duration
if (duration.isNullOrEmpty()) {
result_meta_duration?.isVisible = false
} else {
result_meta_duration?.isVisible = true
result_meta_duration?.text =
if (duration.endsWith("min") || duration.endsWith("h")) duration else "${duration}min"
}
result_meta_site?.text = d.apiName result_meta_site?.text = d.apiName
@ -1188,6 +1270,17 @@ class ResultFragment : Fragment() {
} }
} }
val recAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
SearchAdapter(
ArrayList(),
result_recommendations,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
}
}
result_recommendations.adapter = recAdapter
context?.let { ctx -> context?.let { ctx ->
result_bookmark_button?.isVisible = ctx.isTvSettings() result_bookmark_button?.isVisible = ctx.isTvSettings()

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
@ -85,6 +86,9 @@ class ResultViewModel : ViewModel() {
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
val watchStatus: LiveData<WatchType> get() = _watchStatus val watchStatus: LiveData<WatchType> get() = _watchStatus
private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData()
val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync
fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch { fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch val currentId = id.value ?: return@launch
_watchStatus.postValue(status) _watchStatus.postValue(status)
@ -198,6 +202,17 @@ class ResultViewModel : ViewModel() {
} }
} }
fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch {
if(context == null) return@launch
val list = ArrayList<Resource<SyncAPI.SyncResult?>>()
for (s in sync) {
val result = safeApiCall { s.first.getResult(context, s.second) }
list.add(result)
_sync.postValue(list)
}
}
private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) { private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list) _episodes.postValue(list)
val set = HashMap<Int, Int>() val set = HashMap<Int, Int>()
@ -363,7 +378,7 @@ class ResultViewModel : ViewModel() {
(mainId + index + 1).hashCode(), (mainId + index + 1).hashCode(),
index, index,
i.rating, i.rating,
i.descript, i.description,
null, null,
) )
) )

View file

@ -9,6 +9,7 @@ import android.view.WindowManager
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive
import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive
import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
@ -35,7 +37,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -68,18 +70,11 @@ class SearchFragment : Fragment() {
} }
private fun fixGrid() { private fun fixGrid() {
val compactView = activity?.getGridIsCompact() ?: false activity?.getSpanCount()?.let {
val spanCountLandscape = if (compactView) 2 else 6 currentSpan = it
val spanCountPortrait = if (compactView) 1 else 3
val orientation = resources.configuration.orientation
val currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCountLandscape
} else {
spanCountPortrait
} }
cardSpace.spanCount = currentSpan search_autofit_results.spanCount = currentSpan
HomeFragment.currentSpan = currentSpan currentSpan = currentSpan
HomeFragment.configEvent.invoke(currentSpan) HomeFragment.configEvent.invoke(currentSpan)
} }
@ -102,13 +97,13 @@ class SearchFragment : Fragment() {
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let { val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
SearchAdapter( SearchAdapter(
ArrayList(), ArrayList(),
cardSpace, search_autofit_results,
) { callback -> ) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(activity, callback)
} }
} }
cardSpace.adapter = adapter search_autofit_results.adapter = adapter
search_loading_bar.alpha = 0f search_loading_bar.alpha = 0f
val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
@ -325,7 +320,7 @@ class SearchFragment : Fragment() {
is Resource.Success -> { is Resource.Success -> {
it.value.let { data -> it.value.let { data ->
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
(cardSpace?.adapter as SearchAdapter?)?.apply { (search_autofit_results?.adapter as SearchAdapter?)?.apply {
cardList = data.toList() cardList = data.toList()
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -393,8 +388,8 @@ class SearchFragment : Fragment() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true) val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true)
search_master_recycler.visibility = if (isAdvancedSearch) View.VISIBLE else View.GONE search_master_recycler.isVisible = isAdvancedSearch
cardSpace.visibility = if (!isAdvancedSearch) View.VISIBLE else View.GONE search_autofit_results.isVisible = !isAdvancedSearch
// SubtitlesFragment.push(activity) // SubtitlesFragment.push(activity)
//searchViewModel.search("iron man") //searchViewModel.search("iron man")

View file

@ -146,6 +146,16 @@ object DataStoreHelper {
} }
fun Context.setResultSeason(id: Int, value: Int?) { fun Context.setResultSeason(id: Int, value: Int?) {
return setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) setKey("$currentAccount/$RESULT_SEASON", id.toString(), value)
}
fun Context.addSync(id: Int, idPrefix: String, url: String) {
setKey("${idPrefix}_sync", id.toString(), url)
}
fun Context.getSync(id : Int, idPrefixes : List<String>) : List<String?> {
return idPrefixes.map { idPrefix ->
getKey("${idPrefix}_sync", id.toString())
}
} }
} }

View file

@ -7,6 +7,7 @@ import android.app.AppOpsManager
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
@ -68,6 +69,19 @@ object UIHelper {
) )
} }
fun Activity?.getSpanCount() : Int? {
val compactView = this?.getGridIsCompact() ?: return null
val spanCountLandscape = if (compactView) 2 else 6
val spanCountPortrait = if (compactView) 1 else 3
val orientation = this.resources?.configuration?.orientation ?: return null
return if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
spanCountLandscape
} else {
spanCountPortrait
}
}
fun Fragment.hideKeyboard() { fun Fragment.hideKeyboard() {
activity?.window?.decorView?.clearFocus() activity?.window?.decorView?.clearFocus()
view?.let { view?.let {

View file

@ -12,7 +12,7 @@ object VideoDownloadHelper {
override val id: Int, override val id: Int,
val parentId: Int, val parentId: Int,
val rating: Int?, val rating: Int?,
val descript: String?, val description: String?,
val cacheTime: Long, val cacheTime: Long,
) : EasyDownloadButton.IMinimumData ) : EasyDownloadButton.IMinimumData

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View file

@ -187,9 +187,30 @@
app:mediaRouteButtonTint="?attr/textColor" app:mediaRouteButtonTint="?attr/textColor"
/> />
<ImageView <ImageView
android:visibility="gone"
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_descript" android:nextFocusDown="@id/result_descript"
android:nextFocusLeft="@id/result_back" android:nextFocusLeft="@id/result_back"
android:nextFocusRight="@id/result_share"
android:id="@+id/result_add_sync"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginRight="10dp"
android:elevation="10dp"
android:tint="?attr/textColor"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_add_24"
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/add_sync">
</ImageView>
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_descript"
android:nextFocusLeft="@id/result_add_sync"
android:nextFocusRight="@id/result_openinbrower" android:nextFocusRight="@id/result_openinbrower"
android:id="@+id/result_share" android:id="@+id/result_share"
@ -522,7 +543,32 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
</TextView> </TextView>
</LinearLayout> </LinearLayout>
<com.google.android.material.tabs.TabLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/result_tabs"
app:tabGravity="start"
android:elevation="0dp">
</com.google.android.material.tabs.TabLayout>
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
android:visibility="gone"
android:descendantFocusability="afterDescendants"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:spanCount="3"
android:id="@+id/result_recommendations"
tools:listitem="@layout/search_result_grid"
android:orientation="vertical"
/>
<LinearLayout
android:id="@+id/result_episodes_tab"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:orientation="horizontal" android:orientation="horizontal"
@ -607,6 +653,7 @@
/> />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<LinearLayout <LinearLayout
android:layout_gravity="bottom" android:layout_gravity="bottom"

View file

@ -26,7 +26,7 @@
android:nextFocusUp="@id/nav_rail_view" android:nextFocusUp="@id/nav_rail_view"
android:nextFocusRight="@id/search_filter" android:nextFocusRight="@id/search_filter"
android:nextFocusLeft="@id/nav_rail_view" android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusDown="@id/cardSpace" android:nextFocusDown="@id/search_autofit_results"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:inputType="text" android:inputType="text"
@ -65,7 +65,7 @@
android:nextFocusUp="@id/nav_rail_view" android:nextFocusUp="@id/nav_rail_view"
android:nextFocusRight="@id/main_search" android:nextFocusRight="@id/main_search"
android:nextFocusLeft="@id/main_search" android:nextFocusLeft="@id/main_search"
android:nextFocusDown="@id/cardSpace" android:nextFocusDown="@id/search_autofit_results"
android:id="@+id/search_filter" android:id="@+id/search_filter"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
@ -91,7 +91,7 @@
android:paddingTop="5dp" android:paddingTop="5dp"
app:spanCount="3" app:spanCount="3"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:id="@+id/cardSpace" android:id="@+id/search_autofit_results"
tools:listitem="@layout/search_result_grid" tools:listitem="@layout/search_result_grid"
android:orientation="vertical" android:orientation="vertical"
/> />

View file

@ -34,7 +34,7 @@
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:nextFocusRight="@id/search_filter" android:nextFocusRight="@id/search_filter"
android:nextFocusLeft="@id/search_filter" android:nextFocusLeft="@id/search_filter"
android:nextFocusDown="@id/cardSpace" android:nextFocusDown="@id/search_autofit_results"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:inputType="text" android:inputType="text"

View file

@ -41,7 +41,9 @@
<string name="rew_text_format" translatable="false" formatted="true">-%d</string> <string name="rew_text_format" translatable="false" formatted="true">-%d</string>
<string name="ffw_text_regular_format" translatable="false" formatted="true">%d</string> <string name="ffw_text_regular_format" translatable="false" formatted="true">%d</string>
<string name="rew_text_regular_format" translatable="false" formatted="true">%d</string> <string name="rew_text_regular_format" translatable="false" formatted="true">%d</string>
<string name="app_dub_sub_episode_text_format">%s Ep %d</string> <string name="rating_format" translatable="false" formatted="true">%.1f/10.0</string>
<string name="year_format" translatable="false" formatted="true">%d</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS --> <!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
<string name="result_poster_img_des">Poster</string> <string name="result_poster_img_des">Poster</string>
@ -60,6 +62,7 @@
<string name="rated_format" formatted="true">Rated: %.1f</string> <string name="rated_format" formatted="true">Rated: %.1f</string>
<string name="new_update_format" formatted="true">New update found!\n%s -> %s</string> <string name="new_update_format" formatted="true">New update found!\n%s -> %s</string>
<string name="filler_format" formatted="true">(Filler) %s</string> <string name="filler_format" formatted="true">(Filler) %s</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="app_name">CloudStream</string> <string name="app_name">CloudStream</string>
<string name="title_home">Home</string> <string name="title_home">Home</string>
@ -223,7 +226,7 @@
<string name="cancel" translatable="false">@string/sort_cancel</string> <string name="cancel" translatable="false">@string/sort_cancel</string>
<string name="pause">Pause</string> <string name="pause">Pause</string>
<string name="resume">Resume</string> <string name="resume">Resume</string>
<string name="delete_message">This will permanently delete %s\nAre you sure?</string> <string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string>
<string name="status_ongoing">Ongoing</string> <string name="status_ongoing">Ongoing</string>
<string name="status_completed">Completed</string> <string name="status_completed">Completed</string>
@ -337,12 +340,14 @@
<string name="kitsu_account_settings" translatable="false">Kitsu</string> <string name="kitsu_account_settings" translatable="false">Kitsu</string>
<string name="trakt_account_settings" translatable="false">Trakt</string> <string name="trakt_account_settings" translatable="false">Trakt</string>
--> -->
<string name="login_format">%s %s</string> <string name="login_format" formatted="true">%s %s</string>
<string name="account">account</string> <string name="account">account</string>
<string name="logout">Logout</string> <string name="logout">Logout</string>
<string name="login">Login</string> <string name="login">Login</string>
<string name="switch_account">Switch account</string> <string name="switch_account">Switch account</string>
<string name="add_account">Add account</string> <string name="add_account">Add account</string>
<string name="add_sync">Add tracking</string>
<string name="added_sync_format" formatted="true">Added %s</string>
<!-- ============ --> <!-- ============ -->
<string name="none">None</string> <string name="none">None</string>
<string name="normal">Normal</string> <string name="normal">Normal</string>
@ -361,4 +366,6 @@
https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog
--> -->
<string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string> <string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string>
<string name="tab_recommended">Recommended</string>
</resources> </resources>