forked from recloudstream/cloudstream
staging for sync
This commit is contained in:
parent
04cab02488
commit
cc3eba51f3
25 changed files with 504 additions and 1072 deletions
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() }
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
5
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_add_24.xml
Normal 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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue