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?,
|
||||
val year: Int? = null,
|
||||
val dubStatus: EnumSet<DubStatus>?,
|
||||
val dubStatus: EnumSet<DubStatus>? = null,
|
||||
|
||||
val otherName: String? = null,
|
||||
val dubEpisodes: Int? = null,
|
||||
|
@ -418,7 +418,7 @@ interface LoadResponse {
|
|||
val plot: String?
|
||||
val rating: Int? // 0-100
|
||||
val tags: List<String>?
|
||||
val duration: String?
|
||||
var duration: Int? // in minutes
|
||||
val trailerUrl: String?
|
||||
val recommendations: List<SearchResponse>?
|
||||
}
|
||||
|
@ -444,29 +444,29 @@ data class AnimeEpisode(
|
|||
)
|
||||
|
||||
data class TorrentLoadResponse(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
val magnet: String?,
|
||||
val torrent: String?,
|
||||
override val plot: String?,
|
||||
override val type: TvType = TvType.Torrent,
|
||||
override val posterUrl: String? = null,
|
||||
override val year: Int? = null,
|
||||
override val rating: Int? = null,
|
||||
override val tags: List<String>? = null,
|
||||
override val duration: String? = null,
|
||||
override val trailerUrl: String? = null,
|
||||
override val recommendations: List<SearchResponse>? = null,
|
||||
override var name: String,
|
||||
override var url: String,
|
||||
override var apiName: String,
|
||||
var magnet: String?,
|
||||
var torrent: String?,
|
||||
override var plot: String?,
|
||||
override var type: TvType = TvType.Torrent,
|
||||
override var posterUrl: String? = null,
|
||||
override var year: Int? = null,
|
||||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = null,
|
||||
override var duration: Int? = null,
|
||||
override var trailerUrl: String? = null,
|
||||
override var recommendations: List<SearchResponse>? = null,
|
||||
) : LoadResponse
|
||||
|
||||
data class AnimeLoadResponse(
|
||||
var engName: String? = null,
|
||||
var japName: String? = null,
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override val type: TvType,
|
||||
override var name: String,
|
||||
override var url: String,
|
||||
override var apiName: String,
|
||||
override var type: TvType,
|
||||
|
||||
override var posterUrl: String? = null,
|
||||
override var year: Int? = null,
|
||||
|
@ -481,7 +481,7 @@ data class AnimeLoadResponse(
|
|||
var malId: Int? = null,
|
||||
var anilistId: Int? = null,
|
||||
override var rating: Int? = null,
|
||||
override var duration: String? = null,
|
||||
override var duration: Int? = null,
|
||||
override var trailerUrl: String? = null,
|
||||
override var recommendations: List<SearchResponse>? = null,
|
||||
) : LoadResponse
|
||||
|
@ -503,24 +503,52 @@ fun MainAPI.newAnimeLoadResponse(
|
|||
}
|
||||
|
||||
data class MovieLoadResponse(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override val type: TvType,
|
||||
val dataUrl: String,
|
||||
override var name: String,
|
||||
override var url: String,
|
||||
override var apiName: String,
|
||||
override var type: TvType,
|
||||
var dataUrl: String,
|
||||
|
||||
override val posterUrl: String? = null,
|
||||
override val year: Int? = null,
|
||||
override val plot: String? = null,
|
||||
override var posterUrl: String? = null,
|
||||
override var year: Int? = null,
|
||||
override var plot: String? = null,
|
||||
|
||||
val imdbId: String? = null,
|
||||
override val rating: Int? = null,
|
||||
override val tags: List<String>? = null,
|
||||
override val duration: String? = null,
|
||||
override val trailerUrl: String? = null,
|
||||
override val recommendations: List<SearchResponse>? = null,
|
||||
var imdbId: String? = null,
|
||||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = null,
|
||||
override var duration: Int? = null,
|
||||
override var trailerUrl: String? = null,
|
||||
override var recommendations: List<SearchResponse>? = null,
|
||||
) : 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(
|
||||
val name: String? = null,
|
||||
val season: Int? = null,
|
||||
|
@ -529,25 +557,37 @@ data class TvSeriesEpisode(
|
|||
val posterUrl: String? = null,
|
||||
val date: String? = null,
|
||||
val rating: Int? = null,
|
||||
val descript: String? = null,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
data class TvSeriesLoadResponse(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override val type: TvType,
|
||||
val episodes: List<TvSeriesEpisode>,
|
||||
override var name: String,
|
||||
override var url: String,
|
||||
override var apiName: String,
|
||||
override var type: TvType,
|
||||
var episodes: List<TvSeriesEpisode>,
|
||||
|
||||
override val posterUrl: String? = null,
|
||||
override val year: Int? = null,
|
||||
override val plot: String? = null,
|
||||
override var posterUrl: String? = null,
|
||||
override var year: Int? = null,
|
||||
override var plot: String? = null,
|
||||
|
||||
val showStatus: ShowStatus? = null,
|
||||
val imdbId: String? = null,
|
||||
override val rating: Int? = null,
|
||||
override val tags: List<String>? = null,
|
||||
override val duration: String? = null,
|
||||
override val trailerUrl: String? = null,
|
||||
override val recommendations: List<SearchResponse>? = null,
|
||||
var showStatus: ShowStatus? = null,
|
||||
var imdbId: String? = null,
|
||||
override var rating: Int? = null,
|
||||
override var tags: List<String>? = null,
|
||||
override var duration: Int? = null,
|
||||
override var trailerUrl: String? = null,
|
||||
override var recommendations: List<SearchResponse>? = null,
|
||||
) : 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)
|
||||
}
|
||||
|
||||
fun showToast(act: Activity?, message: String, duration: Int) {
|
||||
if (act == null) return
|
||||
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
|
||||
if (act == null || message == null) return
|
||||
try {
|
||||
currentToast?.cancel()
|
||||
} catch (e: Exception) {
|
||||
|
@ -390,7 +390,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
val toast = Toast(act)
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
toast.duration = duration
|
||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||
toast.view = layout
|
||||
toast.show()
|
||||
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) {
|
||||
japName = japaneseTitle
|
||||
engName = title
|
||||
|
@ -231,6 +253,7 @@ class ZoroProvider : MainAPI() {
|
|||
showStatus = status
|
||||
plot = description
|
||||
this.tags = tags
|
||||
this.recommendations = recommendations
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,10 @@ package com.lagradost.cloudstream3.metaproviders
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.uwetrottmann.tmdb2.Tmdb
|
||||
import com.uwetrottmann.tmdb2.entities.*
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* 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.
|
||||
// Please no stealy.
|
||||
val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb")
|
||||
private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb")
|
||||
|
||||
private fun getImageUrl(link: String?): String? {
|
||||
if (link == null) return null
|
||||
|
@ -81,38 +79,37 @@ open class TmdbProvider : MainAPI() {
|
|||
|
||||
private fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
|
||||
val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 }
|
||||
?.mapNotNull {
|
||||
it.episodes?.map {
|
||||
?.mapNotNull { season ->
|
||||
season.episodes?.map { episode ->
|
||||
TvSeriesEpisode(
|
||||
it.name,
|
||||
it.season_number,
|
||||
it.episode_number,
|
||||
episode.name,
|
||||
episode.season_number,
|
||||
episode.episode_number,
|
||||
TmdbLink(
|
||||
it.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
|
||||
episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
|
||||
this.id,
|
||||
it.episode_number,
|
||||
it.season_number,
|
||||
episode.episode_number,
|
||||
episode.season_number,
|
||||
).toJson(),
|
||||
getImageUrl(it.still_path),
|
||||
it.air_date?.toString(),
|
||||
it.rating,
|
||||
it.overview,
|
||||
getImageUrl(episode.still_path),
|
||||
episode.air_date?.toString(),
|
||||
episode.rating,
|
||||
episode.overview,
|
||||
)
|
||||
} ?: (1..(it.episode_count ?: 1)).map { episodeNum ->
|
||||
} ?: (1..(season.episode_count ?: 1)).map { episodeNum ->
|
||||
TvSeriesEpisode(
|
||||
episode = episodeNum,
|
||||
data = TmdbLink(
|
||||
this.external_ids?.imdb_id,
|
||||
this.id,
|
||||
episodeNum,
|
||||
it.season_number,
|
||||
season.season_number,
|
||||
).toJson(),
|
||||
season = it.season_number
|
||||
season = season.season_number
|
||||
)
|
||||
}
|
||||
}?.flatten() ?: listOf()
|
||||
|
||||
// println("STATUS ${this.status}")
|
||||
return TvSeriesLoadResponse(
|
||||
this.name ?: this.original_name,
|
||||
getUrl(id, true),
|
||||
|
@ -130,7 +127,7 @@ open class TmdbProvider : MainAPI() {
|
|||
this.external_ids?.imdb_id,
|
||||
this.rating,
|
||||
this.genres?.mapNotNull { it.name },
|
||||
this.episode_run_time?.average()?.times(60)?.toInt()?.let { secondsToReadable(it, "") },
|
||||
this.episode_run_time?.average()?.toInt(),
|
||||
null,
|
||||
this.recommendations?.results?.map { it.toSearchResponse() }
|
||||
)
|
||||
|
@ -158,7 +155,7 @@ open class TmdbProvider : MainAPI() {
|
|||
null,//this.status
|
||||
this.rating,
|
||||
this.genres?.mapNotNull { it.name },
|
||||
this.runtime?.times(60)?.let { secondsToReadable(it, "") },
|
||||
this.runtime,
|
||||
null,
|
||||
this.recommendations?.results?.map { it.toSearchResponse() }
|
||||
)
|
||||
|
|
|
@ -136,19 +136,13 @@ class AllMoviesForYouProvider : MainAPI() {
|
|||
val data = getLink(document)
|
||||
?: throw ErrorLoadingException("No Links Found")
|
||||
|
||||
return MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
type,
|
||||
mapper.writeValueAsString(data.filter { it != "about:blank" }),
|
||||
backgroundPoster,
|
||||
year?.toIntOrNull(),
|
||||
descipt,
|
||||
null,
|
||||
rating,
|
||||
duration = duration
|
||||
)
|
||||
return newMovieLoadResponse(title,url,type,mapper.writeValueAsString(data.filter { it != "about:blank" })) {
|
||||
posterUrl = backgroundPoster
|
||||
this.year = year?.toIntOrNull()
|
||||
this.plot = descipt
|
||||
this.rating = rating
|
||||
setDuration(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
|
|||
override val hasDownloadSupport = true
|
||||
override val usesWebView = true
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
)
|
||||
|
||||
private fun Element.toSearchResult(): SearchResponse {
|
||||
val img = this.select("img")
|
||||
|
@ -162,22 +162,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
|
|||
|
||||
val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/")
|
||||
|
||||
return MovieLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
TvType.Movie,
|
||||
webViewUrl,
|
||||
posterUrl,
|
||||
year,
|
||||
plot,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
duration,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return newMovieLoadResponse(title, url, TvType.Movie, webViewUrl) {
|
||||
this.year = year
|
||||
this.posterUrl = posterUrl
|
||||
this.plot = plot
|
||||
setDuration(duration)
|
||||
}
|
||||
} else {
|
||||
val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text
|
||||
val seasonsDocument = Jsoup.parse(seasonsHtml)
|
||||
|
@ -212,23 +202,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() {
|
|||
}
|
||||
|
||||
}
|
||||
return TvSeriesLoadResponse(
|
||||
title,
|
||||
url,
|
||||
this.name,
|
||||
TvType.TvSeries,
|
||||
episodes,
|
||||
posterUrl,
|
||||
year,
|
||||
plot,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
duration,
|
||||
null,
|
||||
null
|
||||
)
|
||||
return newTvSeriesLoadResponse(title,url,TvType.TvSeries,episodes) {
|
||||
this.posterUrl = posterUrl
|
||||
this.year = year
|
||||
this.plot = plot
|
||||
setDuration(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
val vudoResponse = app.get(url).text
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,6 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKeys
|
|||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
|
||||
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
|
||||
protected val accountId get() = "${idPrefix}_account_$accountIndex"
|
||||
private val accountActiveKey get() = "${idPrefix}_active"
|
||||
|
|
|
@ -10,6 +10,9 @@ interface OAuth2API {
|
|||
val name: String
|
||||
val redirectUrl: String
|
||||
|
||||
// don't change this as all keys depend on it
|
||||
val idPrefix : String
|
||||
|
||||
fun handleRedirect(context: Context, url: String)
|
||||
fun authenticate(context: Context)
|
||||
|
||||
|
|
|
@ -5,11 +5,10 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
|||
|
||||
//TODO dropbox sync
|
||||
class Dropbox : OAuth2API {
|
||||
override val idPrefix = "dropbox"
|
||||
override val name = "Dropbox"
|
||||
override val key: String
|
||||
get() = "zlqsamadlwydvb2"
|
||||
override val redirectUrl: String
|
||||
get() = "dropboxlogin"
|
||||
override val key = "zlqsamadlwydvb2"
|
||||
override val redirectUrl = "dropboxlogin"
|
||||
|
||||
override fun authenticate(context: Context) {
|
||||
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.fixPaddingStatusbar
|
||||
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.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -124,15 +123,8 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun fixGrid() {
|
||||
val compactView = activity?.getGridIsCompact() ?: false
|
||||
val spanCountLandscape = if (compactView) 2 else 6
|
||||
val spanCountPortrait = if (compactView) 1 else 3
|
||||
val orientation = resources.configuration.orientation
|
||||
|
||||
currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCountLandscape
|
||||
} else {
|
||||
spanCountPortrait
|
||||
activity?.getSpanCount()?.let {
|
||||
currentSpan = it
|
||||
}
|
||||
configEvent.invoke(currentSpan)
|
||||
}
|
||||
|
|
|
@ -19,11 +19,8 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
||||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.*
|
||||
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.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
|
@ -34,12 +31,22 @@ import java.util.concurrent.locks.ReentrantLock
|
|||
|
||||
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||
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 {
|
||||
putBoolean("mainapi", mainApi)
|
||||
putBoolean("mainapi", true)
|
||||
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()
|
||||
|
@ -56,6 +63,11 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
|||
return inflater.inflate(R.layout.quick_search, container, false)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
clickCallback = null
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(quick_search_root)
|
||||
|
@ -96,7 +108,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
|||
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
} else {
|
||||
//TODO MAL RESPONSE
|
||||
clickCallback?.invoke(callback)
|
||||
}
|
||||
}
|
||||
else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
|
@ -135,7 +147,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
|||
is Resource.Success -> {
|
||||
it.value.let { data ->
|
||||
if (data.isNotEmpty()) {
|
||||
(cardSpace?.adapter as SearchAdapter?)?.apply {
|
||||
(search_autofit_results?.adapter as SearchAdapter?)?.apply {
|
||||
cardList = data.toList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.content.Context.CLIPBOARD_SERVICE
|
|||
import android.content.Intent
|
||||
import android.content.Intent.*
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -15,7 +16,9 @@ import android.view.View
|
|||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.FileProvider
|
||||
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.normalSafeApiCall
|
||||
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.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||
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.PlayerFragment
|
||||
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.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
|
||||
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.DataStore.getFolderName
|
||||
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.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
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.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
|
@ -256,7 +266,65 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
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) {
|
||||
if (!show || currentType?.isMovieType() == false) {
|
||||
|
@ -273,6 +341,7 @@ class ResultFragment : Fragment() {
|
|||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
fixGrid()
|
||||
|
||||
val restart = arguments?.getBoolean("restart") ?: false
|
||||
if (restart) {
|
||||
|
@ -949,6 +1018,20 @@ class ResultFragment : Fragment() {
|
|||
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 ->
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
|
@ -965,7 +1048,7 @@ class ResultFragment : Fragment() {
|
|||
VPNStatus.Torrent -> getString(R.string.vpn_torrent)
|
||||
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) {
|
||||
ProviderType.MetaProvider -> getString(R.string.provider_info_meta)
|
||||
|
@ -973,8 +1056,6 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
result_info?.isVisible = api.providerType == ProviderType.MetaProvider
|
||||
|
||||
//result_bookmark_button.text = getString(R.string.type_watching)
|
||||
|
||||
currentHeaderName = d.name
|
||||
currentType = d.type
|
||||
|
||||
|
@ -992,7 +1073,7 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
|
||||
result_search?.setOnClickListener {
|
||||
QuickSearchFragment.push(activity, true, d.name)
|
||||
QuickSearchFragment.pushSearch(activity, d.name)
|
||||
}
|
||||
|
||||
result_share?.setOnClickListener {
|
||||
|
@ -1003,6 +1084,20 @@ class ResultFragment : Fragment() {
|
|||
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>>()
|
||||
if (d is AnimeLoadResponse) {
|
||||
val status = when (d.showStatus) {
|
||||
|
@ -1015,23 +1110,10 @@ class ResultFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
result_meta_year?.isGone = d.year == null
|
||||
result_meta_year?.text = d.year?.toString() ?: ""
|
||||
if (d.rating == null) {
|
||||
result_meta_rating?.isVisible = false
|
||||
} 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"
|
||||
}
|
||||
setDuration(d.duration)
|
||||
setYear(d.year)
|
||||
setRating(d.rating)
|
||||
setRecommendations(d.recommendations)
|
||||
|
||||
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 ->
|
||||
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.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
@ -85,6 +86,9 @@ class ResultViewModel : ViewModel() {
|
|||
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
|
||||
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 {
|
||||
val currentId = id.value ?: return@launch
|
||||
_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?) {
|
||||
_episodes.postValue(list)
|
||||
val set = HashMap<Int, Int>()
|
||||
|
@ -363,7 +378,7 @@ class ResultViewModel : ViewModel() {
|
|||
(mainId + index + 1).hashCode(),
|
||||
index,
|
||||
i.rating,
|
||||
i.descript,
|
||||
i.description,
|
||||
null,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.view.WindowManager
|
|||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
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.typesActive
|
||||
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.ParentItemAdapter
|
||||
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.UIHelper.dismissSafe
|
||||
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 kotlinx.android.synthetic.main.fragment_search.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
@ -68,18 +70,11 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun fixGrid() {
|
||||
val compactView = activity?.getGridIsCompact() ?: false
|
||||
val spanCountLandscape = if (compactView) 2 else 6
|
||||
val spanCountPortrait = if (compactView) 1 else 3
|
||||
val orientation = resources.configuration.orientation
|
||||
|
||||
val currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCountLandscape
|
||||
} else {
|
||||
spanCountPortrait
|
||||
activity?.getSpanCount()?.let {
|
||||
currentSpan = it
|
||||
}
|
||||
cardSpace.spanCount = currentSpan
|
||||
HomeFragment.currentSpan = currentSpan
|
||||
search_autofit_results.spanCount = currentSpan
|
||||
currentSpan = currentSpan
|
||||
HomeFragment.configEvent.invoke(currentSpan)
|
||||
}
|
||||
|
||||
|
@ -102,13 +97,13 @@ class SearchFragment : Fragment() {
|
|||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
|
||||
SearchAdapter(
|
||||
ArrayList(),
|
||||
cardSpace,
|
||||
search_autofit_results,
|
||||
) { callback ->
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
}
|
||||
}
|
||||
|
||||
cardSpace.adapter = adapter
|
||||
search_autofit_results.adapter = adapter
|
||||
search_loading_bar.alpha = 0f
|
||||
|
||||
val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
|
@ -325,7 +320,7 @@ class SearchFragment : Fragment() {
|
|||
is Resource.Success -> {
|
||||
it.value.let { data ->
|
||||
if (data.isNotEmpty()) {
|
||||
(cardSpace?.adapter as SearchAdapter?)?.apply {
|
||||
(search_autofit_results?.adapter as SearchAdapter?)?.apply {
|
||||
cardList = data.toList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
@ -393,8 +388,8 @@ class SearchFragment : Fragment() {
|
|||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true)
|
||||
|
||||
search_master_recycler.visibility = if (isAdvancedSearch) View.VISIBLE else View.GONE
|
||||
cardSpace.visibility = if (!isAdvancedSearch) View.VISIBLE else View.GONE
|
||||
search_master_recycler.isVisible = isAdvancedSearch
|
||||
search_autofit_results.isVisible = !isAdvancedSearch
|
||||
|
||||
// SubtitlesFragment.push(activity)
|
||||
//searchViewModel.search("iron man")
|
||||
|
|
|
@ -118,7 +118,7 @@ object DataStoreHelper {
|
|||
|
||||
fun Context.setViewPos(id: Int?, pos: Long, dur: Long) {
|
||||
if (id == null) return
|
||||
if(dur < 10_000) return // too short
|
||||
if (dur < 10_000) return // too short
|
||||
setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur))
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,16 @@ object DataStoreHelper {
|
|||
}
|
||||
|
||||
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.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
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() {
|
||||
activity?.window?.decorView?.clearFocus()
|
||||
view?.let {
|
||||
|
|
|
@ -12,7 +12,7 @@ object VideoDownloadHelper {
|
|||
override val id: Int,
|
||||
val parentId: Int,
|
||||
val rating: Int?,
|
||||
val descript: String?,
|
||||
val description: String?,
|
||||
val cacheTime: Long,
|
||||
) : 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"
|
||||
/>
|
||||
<ImageView
|
||||
android:visibility="gone"
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_descript"
|
||||
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:id="@+id/result_share"
|
||||
|
@ -522,89 +543,115 @@
|
|||
android:layout_height="match_parent">
|
||||
</TextView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="Season 1"
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_episode_select"
|
||||
android:nextFocusLeft="@id/result_episode_select"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_season_button"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton">
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="50-100"
|
||||
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_episode_select"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="Dubbed"
|
||||
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_dub_select"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton"
|
||||
/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/result_episodes_text"
|
||||
tools:text="8 Episodes"
|
||||
android:textSize="17sp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="normal"
|
||||
android:textColor="?attr/textColor"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/result_episode_loading"
|
||||
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:id="@+id/result_episodes"
|
||||
android:clipToPadding="false"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingBottom="100dp"
|
||||
tools:listitem="@layout/result_episode"
|
||||
<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
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="Season 1"
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_episode_select"
|
||||
android:nextFocusLeft="@id/result_episode_select"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_season_button"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton">
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="50-100"
|
||||
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_episode_select"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
tools:text="Dubbed"
|
||||
|
||||
android:nextFocusUp="@id/result_descript"
|
||||
android:nextFocusRight="@id/result_season_button"
|
||||
android:nextFocusLeft="@id/result_season_button"
|
||||
android:nextFocusDown="@id/result_episodes"
|
||||
|
||||
android:id="@+id/result_dub_select"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="0dp"
|
||||
style="@style/MultiSelectButton"
|
||||
/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/result_episodes_text"
|
||||
tools:text="8 Episodes"
|
||||
android:textSize="17sp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textStyle="normal"
|
||||
android:textColor="?attr/textColor"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/result_episode_loading"
|
||||
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:id="@+id/result_episodes"
|
||||
android:clipToPadding="false"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingBottom="100dp"
|
||||
tools:listitem="@layout/result_episode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/search_filter"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/cardSpace"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
|
@ -65,7 +65,7 @@
|
|||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusDown="@id/cardSpace"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
|
||||
android:id="@+id/search_filter"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
|
@ -91,7 +91,7 @@
|
|||
android:paddingTop="5dp"
|
||||
app:spanCount="3"
|
||||
android:paddingEnd="8dp"
|
||||
android:id="@+id/cardSpace"
|
||||
android:id="@+id/search_autofit_results"
|
||||
tools:listitem="@layout/search_result_grid"
|
||||
android:orientation="vertical"
|
||||
/>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<androidx.appcompat.widget.SearchView
|
||||
android:nextFocusRight="@id/search_filter"
|
||||
android:nextFocusLeft="@id/search_filter"
|
||||
android:nextFocusDown="@id/cardSpace"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
|
|
|
@ -41,7 +41,9 @@
|
|||
<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="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 -->
|
||||
<string name="result_poster_img_des">Poster</string>
|
||||
|
@ -60,6 +62,7 @@
|
|||
<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="filler_format" formatted="true">(Filler) %s</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="title_home">Home</string>
|
||||
|
@ -223,7 +226,7 @@
|
|||
<string name="cancel" translatable="false">@string/sort_cancel</string>
|
||||
<string name="pause">Pause</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_completed">Completed</string>
|
||||
|
@ -337,12 +340,14 @@
|
|||
<string name="kitsu_account_settings" translatable="false">Kitsu</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="logout">Logout</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="switch_account">Switch 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="normal">Normal</string>
|
||||
|
@ -361,4 +366,6 @@
|
|||
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="tab_recommended">Recommended</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue