staging for sync

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

View File

@ -367,7 +367,7 @@ data class AnimeSearchResponse(
override val posterUrl: String?,
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
}

View File

@ -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

View File

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

View File

@ -2,12 +2,10 @@ package com.lagradost.cloudstream3.metaproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.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() }
)

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

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

View File

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

View File

@ -44,11 +44,11 @@ class VfSerieProvider : MainAPI() {
private fun getDirect(original: String): String { // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example
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
}

View File

@ -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"

View File

@ -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)

View File

@ -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")

View File

@ -48,12 +48,11 @@ import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.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)
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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,
)
)

View File

@ -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")

View File

@ -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())
}
}
}

View File

@ -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 {

View File

@ -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

View File

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

View File

@ -187,9 +187,30 @@
app:mediaRouteButtonTint="?attr/textColor"
/>
<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>

View File

@ -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"
/>

View File

@ -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"

View File

@ -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>