forked from recloudstream/cloudstream
		
	staging for sync
This commit is contained in:
		
							parent
							
								
									04cab02488
								
							
						
					
					
						commit
						cc3eba51f3
					
				
					 25 changed files with 504 additions and 1072 deletions
				
			
		|  | @ -367,7 +367,7 @@ data class AnimeSearchResponse( | ||||||
| 
 | 
 | ||||||
|     override val posterUrl: String?, |     override val posterUrl: String?, | ||||||
|     val year: Int? = null, |     val year: Int? = null, | ||||||
|     val dubStatus: EnumSet<DubStatus>?, |     val dubStatus: EnumSet<DubStatus>? = null, | ||||||
| 
 | 
 | ||||||
|     val otherName: String? = null, |     val otherName: String? = null, | ||||||
|     val dubEpisodes: Int? = null, |     val dubEpisodes: Int? = null, | ||||||
|  | @ -418,7 +418,7 @@ interface LoadResponse { | ||||||
|     val plot: String? |     val plot: String? | ||||||
|     val rating: Int? // 0-100 |     val rating: Int? // 0-100 | ||||||
|     val tags: List<String>? |     val tags: List<String>? | ||||||
|     val duration: String? |     var duration: Int? // in minutes | ||||||
|     val trailerUrl: String? |     val trailerUrl: String? | ||||||
|     val recommendations: List<SearchResponse>? |     val recommendations: List<SearchResponse>? | ||||||
| } | } | ||||||
|  | @ -444,29 +444,29 @@ data class AnimeEpisode( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| data class TorrentLoadResponse( | data class TorrentLoadResponse( | ||||||
|     override val name: String, |     override var name: String, | ||||||
|     override val url: String, |     override var url: String, | ||||||
|     override val apiName: String, |     override var apiName: String, | ||||||
|     val magnet: String?, |     var magnet: String?, | ||||||
|     val torrent: String?, |     var torrent: String?, | ||||||
|     override val plot: String?, |     override var plot: String?, | ||||||
|     override val type: TvType = TvType.Torrent, |     override var type: TvType = TvType.Torrent, | ||||||
|     override val posterUrl: String? = null, |     override var posterUrl: String? = null, | ||||||
|     override val year: Int? = null, |     override var year: Int? = null, | ||||||
|     override val rating: Int? = null, |     override var rating: Int? = null, | ||||||
|     override val tags: List<String>? = null, |     override var tags: List<String>? = null, | ||||||
|     override val duration: String? = null, |     override var duration: Int? = null, | ||||||
|     override val trailerUrl: String? = null, |     override var trailerUrl: String? = null, | ||||||
|     override val recommendations: List<SearchResponse>? = null, |     override var recommendations: List<SearchResponse>? = null, | ||||||
| ) : LoadResponse | ) : LoadResponse | ||||||
| 
 | 
 | ||||||
| data class AnimeLoadResponse( | data class AnimeLoadResponse( | ||||||
|     var engName: String? = null, |     var engName: String? = null, | ||||||
|     var japName: String? = null, |     var japName: String? = null, | ||||||
|     override val name: String, |     override var name: String, | ||||||
|     override val url: String, |     override var url: String, | ||||||
|     override val apiName: String, |     override var apiName: String, | ||||||
|     override val type: TvType, |     override var type: TvType, | ||||||
| 
 | 
 | ||||||
|     override var posterUrl: String? = null, |     override var posterUrl: String? = null, | ||||||
|     override var year: Int? = null, |     override var year: Int? = null, | ||||||
|  | @ -481,7 +481,7 @@ data class AnimeLoadResponse( | ||||||
|     var malId: Int? = null, |     var malId: Int? = null, | ||||||
|     var anilistId: Int? = null, |     var anilistId: Int? = null, | ||||||
|     override var rating: Int? = null, |     override var rating: Int? = null, | ||||||
|     override var duration: String? = null, |     override var duration: Int? = null, | ||||||
|     override var trailerUrl: String? = null, |     override var trailerUrl: String? = null, | ||||||
|     override var recommendations: List<SearchResponse>? = null, |     override var recommendations: List<SearchResponse>? = null, | ||||||
| ) : LoadResponse | ) : LoadResponse | ||||||
|  | @ -503,24 +503,52 @@ fun MainAPI.newAnimeLoadResponse( | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| data class MovieLoadResponse( | data class MovieLoadResponse( | ||||||
|     override val name: String, |     override var name: String, | ||||||
|     override val url: String, |     override var url: String, | ||||||
|     override val apiName: String, |     override var apiName: String, | ||||||
|     override val type: TvType, |     override var type: TvType, | ||||||
|     val dataUrl: String, |     var dataUrl: String, | ||||||
| 
 | 
 | ||||||
|     override val posterUrl: String? = null, |     override var posterUrl: String? = null, | ||||||
|     override val year: Int? = null, |     override var year: Int? = null, | ||||||
|     override val plot: String? = null, |     override var plot: String? = null, | ||||||
| 
 | 
 | ||||||
|     val imdbId: String? = null, |     var imdbId: String? = null, | ||||||
|     override val rating: Int? = null, |     override var rating: Int? = null, | ||||||
|     override val tags: List<String>? = null, |     override var tags: List<String>? = null, | ||||||
|     override val duration: String? = null, |     override var duration: Int? = null, | ||||||
|     override val trailerUrl: String? = null, |     override var trailerUrl: String? = null, | ||||||
|     override val recommendations: List<SearchResponse>? = null, |     override var recommendations: List<SearchResponse>? = null, | ||||||
| ) : LoadResponse | ) : LoadResponse | ||||||
| 
 | 
 | ||||||
|  | fun MainAPI.newMovieLoadResponse( | ||||||
|  |     name: String, | ||||||
|  |     url: String, | ||||||
|  |     type: TvType, | ||||||
|  |     dataUrl : String, | ||||||
|  |     initializer: MovieLoadResponse.() -> Unit = { } | ||||||
|  | ): MovieLoadResponse { | ||||||
|  |     val builder = MovieLoadResponse(name = name, url = url, apiName = this.name, type = type,dataUrl = dataUrl) | ||||||
|  |     builder.initializer() | ||||||
|  |     return builder | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fun LoadResponse.setDuration(input : String?) { | ||||||
|  |     if (input == null) return | ||||||
|  |     Regex("([0-9]*)h.*?([0-9]*)m").matchEntire(input)?.groupValues?.let { values -> | ||||||
|  |         if(values.size == 3) { | ||||||
|  |             val hours = values[1].toIntOrNull() | ||||||
|  |             val minutes = values[2].toIntOrNull() | ||||||
|  |             this.duration = if(minutes != null && hours != null) { hours * 60 + minutes } else null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     Regex("([0-9]*)m").matchEntire(input)?.groupValues?.let { values -> | ||||||
|  |         if(values.size == 2) { | ||||||
|  |             this.duration = values[1].toIntOrNull() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| data class TvSeriesEpisode( | data class TvSeriesEpisode( | ||||||
|     val name: String? = null, |     val name: String? = null, | ||||||
|     val season: Int? = null, |     val season: Int? = null, | ||||||
|  | @ -529,25 +557,37 @@ data class TvSeriesEpisode( | ||||||
|     val posterUrl: String? = null, |     val posterUrl: String? = null, | ||||||
|     val date: String? = null, |     val date: String? = null, | ||||||
|     val rating: Int? = null, |     val rating: Int? = null, | ||||||
|     val descript: String? = null, |     val description: String? = null, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| data class TvSeriesLoadResponse( | data class TvSeriesLoadResponse( | ||||||
|     override val name: String, |     override var name: String, | ||||||
|     override val url: String, |     override var url: String, | ||||||
|     override val apiName: String, |     override var apiName: String, | ||||||
|     override val type: TvType, |     override var type: TvType, | ||||||
|     val episodes: List<TvSeriesEpisode>, |     var episodes: List<TvSeriesEpisode>, | ||||||
| 
 | 
 | ||||||
|     override val posterUrl: String? = null, |     override var posterUrl: String? = null, | ||||||
|     override val year: Int? = null, |     override var year: Int? = null, | ||||||
|     override val plot: String? = null, |     override var plot: String? = null, | ||||||
| 
 | 
 | ||||||
|     val showStatus: ShowStatus? = null, |     var showStatus: ShowStatus? = null, | ||||||
|     val imdbId: String? = null, |     var imdbId: String? = null, | ||||||
|     override val rating: Int? = null, |     override var rating: Int? = null, | ||||||
|     override val tags: List<String>? = null, |     override var tags: List<String>? = null, | ||||||
|     override val duration: String? = null, |     override var duration: Int? = null, | ||||||
|     override val trailerUrl: String? = null, |     override var trailerUrl: String? = null, | ||||||
|     override val recommendations: List<SearchResponse>? = null, |     override var recommendations: List<SearchResponse>? = null, | ||||||
| ) : LoadResponse | ) : LoadResponse | ||||||
|  | 
 | ||||||
|  | fun MainAPI.newTvSeriesLoadResponse( | ||||||
|  |     name: String, | ||||||
|  |     url: String, | ||||||
|  |     type: TvType, | ||||||
|  |     episodes : List<TvSeriesEpisode>, | ||||||
|  |     initializer: TvSeriesLoadResponse.() -> Unit = { } | ||||||
|  | ): TvSeriesLoadResponse { | ||||||
|  |     val builder = TvSeriesLoadResponse(name = name, url = url, apiName = this.name, type = type, episodes = episodes) | ||||||
|  |     builder.initializer() | ||||||
|  |     return builder | ||||||
|  | } | ||||||
|  | @ -370,8 +370,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|             showToast(act, act.getString(message), duration) |             showToast(act, act.getString(message), duration) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fun showToast(act: Activity?, message: String, duration: Int) { |         fun showToast(act: Activity?, message: String?, duration: Int? = null) { | ||||||
|             if (act == null) return |             if (act == null || message == null) return | ||||||
|             try { |             try { | ||||||
|                 currentToast?.cancel() |                 currentToast?.cancel() | ||||||
|             } catch (e: Exception) { |             } catch (e: Exception) { | ||||||
|  | @ -390,7 +390,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
| 
 | 
 | ||||||
|                 val toast = Toast(act) |                 val toast = Toast(act) | ||||||
|                 toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) |                 toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) | ||||||
|                 toast.duration = duration |                 toast.duration = duration ?: Toast.LENGTH_SHORT | ||||||
|                 toast.view = layout |                 toast.view = layout | ||||||
|                 toast.show() |                 toast.show() | ||||||
|                 currentToast = toast |                 currentToast = toast | ||||||
|  |  | ||||||
|  | @ -222,6 +222,28 @@ class ZoroProvider : MainAPI() { | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         val recommendations = | ||||||
|  |             document.select("#main-content > section > .tab-content > div > .film_list-wrap > .flw-item") | ||||||
|  |                 .mapNotNull { head -> | ||||||
|  |                     val filmPoster = head?.selectFirst(".film-poster") | ||||||
|  |                     val epPoster = filmPoster?.selectFirst("img")?.attr("data-src") | ||||||
|  |                     val a = head?.selectFirst(".film-detail > .film-name > a") | ||||||
|  |                     val epHref = a?.attr("href") | ||||||
|  |                     val epTitle = a?.attr("title") | ||||||
|  |                     if (epHref == null || epTitle == null || epPoster == null) { | ||||||
|  |                         null | ||||||
|  |                     } else { | ||||||
|  |                         AnimeSearchResponse( | ||||||
|  |                             epTitle, | ||||||
|  |                             fixUrl(epHref), | ||||||
|  |                             this.name, | ||||||
|  |                             TvType.Anime, | ||||||
|  |                             epPoster, | ||||||
|  |                             dubStatus = null | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|         return newAnimeLoadResponse(title, url, TvType.Anime) { |         return newAnimeLoadResponse(title, url, TvType.Anime) { | ||||||
|             japName = japaneseTitle |             japName = japaneseTitle | ||||||
|             engName = title |             engName = title | ||||||
|  | @ -231,6 +253,7 @@ class ZoroProvider : MainAPI() { | ||||||
|             showStatus = status |             showStatus = status | ||||||
|             plot = description |             plot = description | ||||||
|             this.tags = tags |             this.tags = tags | ||||||
|  |             this.recommendations = recommendations | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,12 +2,10 @@ package com.lagradost.cloudstream3.metaproviders | ||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
| import com.lagradost.cloudstream3.* | import com.lagradost.cloudstream3.* | ||||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable |  | ||||||
| import com.lagradost.cloudstream3.utils.AppUtils.toJson | import com.lagradost.cloudstream3.utils.AppUtils.toJson | ||||||
| import com.uwetrottmann.tmdb2.Tmdb | import com.uwetrottmann.tmdb2.Tmdb | ||||||
| import com.uwetrottmann.tmdb2.entities.* | import com.uwetrottmann.tmdb2.entities.* | ||||||
| import java.util.* | import java.util.* | ||||||
| import kotlin.math.roundToInt |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * episode and season starting from 1 |  * episode and season starting from 1 | ||||||
|  | @ -34,7 +32,7 @@ open class TmdbProvider : MainAPI() { | ||||||
| 
 | 
 | ||||||
|     // Fuck it, public private api key because github actions won't co-operate. |     // Fuck it, public private api key because github actions won't co-operate. | ||||||
|     // Please no stealy. |     // Please no stealy. | ||||||
|     val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") |     private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb") | ||||||
| 
 | 
 | ||||||
|     private fun getImageUrl(link: String?): String? { |     private fun getImageUrl(link: String?): String? { | ||||||
|         if (link == null) return null |         if (link == null) return null | ||||||
|  | @ -81,38 +79,37 @@ open class TmdbProvider : MainAPI() { | ||||||
| 
 | 
 | ||||||
|     private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { |     private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { | ||||||
|         val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } |         val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } | ||||||
|             ?.mapNotNull { |             ?.mapNotNull { season -> | ||||||
|                 it.episodes?.map { |                 season.episodes?.map { episode -> | ||||||
|                     TvSeriesEpisode( |                     TvSeriesEpisode( | ||||||
|                         it.name, |                         episode.name, | ||||||
|                         it.season_number, |                         episode.season_number, | ||||||
|                         it.episode_number, |                         episode.episode_number, | ||||||
|                         TmdbLink( |                         TmdbLink( | ||||||
|                             it.external_ids?.imdb_id ?: this.external_ids?.imdb_id, |                             episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id, | ||||||
|                             this.id, |                             this.id, | ||||||
|                             it.episode_number, |                             episode.episode_number, | ||||||
|                             it.season_number, |                             episode.season_number, | ||||||
|                         ).toJson(), |                         ).toJson(), | ||||||
|                         getImageUrl(it.still_path), |                         getImageUrl(episode.still_path), | ||||||
|                         it.air_date?.toString(), |                         episode.air_date?.toString(), | ||||||
|                         it.rating, |                         episode.rating, | ||||||
|                         it.overview, |                         episode.overview, | ||||||
|                     ) |                     ) | ||||||
|                 } ?: (1..(it.episode_count ?: 1)).map { episodeNum -> |                 } ?: (1..(season.episode_count ?: 1)).map { episodeNum -> | ||||||
|                     TvSeriesEpisode( |                     TvSeriesEpisode( | ||||||
|                         episode = episodeNum, |                         episode = episodeNum, | ||||||
|                         data = TmdbLink( |                         data = TmdbLink( | ||||||
|                             this.external_ids?.imdb_id, |                             this.external_ids?.imdb_id, | ||||||
|                             this.id, |                             this.id, | ||||||
|                             episodeNum, |                             episodeNum, | ||||||
|                             it.season_number, |                             season.season_number, | ||||||
|                         ).toJson(), |                         ).toJson(), | ||||||
|                         season = it.season_number |                         season = season.season_number | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             }?.flatten() ?: listOf() |             }?.flatten() ?: listOf() | ||||||
| 
 | 
 | ||||||
| //        println("STATUS ${this.status}") |  | ||||||
|         return TvSeriesLoadResponse( |         return TvSeriesLoadResponse( | ||||||
|             this.name ?: this.original_name, |             this.name ?: this.original_name, | ||||||
|             getUrl(id, true), |             getUrl(id, true), | ||||||
|  | @ -130,7 +127,7 @@ open class TmdbProvider : MainAPI() { | ||||||
|             this.external_ids?.imdb_id, |             this.external_ids?.imdb_id, | ||||||
|             this.rating, |             this.rating, | ||||||
|             this.genres?.mapNotNull { it.name }, |             this.genres?.mapNotNull { it.name }, | ||||||
|             this.episode_run_time?.average()?.times(60)?.toInt()?.let { secondsToReadable(it, "") }, |             this.episode_run_time?.average()?.toInt(), | ||||||
|             null, |             null, | ||||||
|             this.recommendations?.results?.map { it.toSearchResponse() } |             this.recommendations?.results?.map { it.toSearchResponse() } | ||||||
|         ) |         ) | ||||||
|  | @ -158,7 +155,7 @@ open class TmdbProvider : MainAPI() { | ||||||
|             null,//this.status |             null,//this.status | ||||||
|             this.rating, |             this.rating, | ||||||
|             this.genres?.mapNotNull { it.name }, |             this.genres?.mapNotNull { it.name }, | ||||||
|             this.runtime?.times(60)?.let { secondsToReadable(it, "") }, |             this.runtime, | ||||||
|             null, |             null, | ||||||
|             this.recommendations?.results?.map { it.toSearchResponse() } |             this.recommendations?.results?.map { it.toSearchResponse() } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -136,19 +136,13 @@ class AllMoviesForYouProvider : MainAPI() { | ||||||
|             val data = getLink(document) |             val data = getLink(document) | ||||||
|                 ?: throw ErrorLoadingException("No Links Found") |                 ?: throw ErrorLoadingException("No Links Found") | ||||||
| 
 | 
 | ||||||
|             return MovieLoadResponse( |             return newMovieLoadResponse(title,url,type,mapper.writeValueAsString(data.filter { it != "about:blank" })) { | ||||||
|                 title, |                posterUrl = backgroundPoster | ||||||
|                 url, |                 this.year = year?.toIntOrNull() | ||||||
|                 this.name, |                 this.plot = descipt | ||||||
|                 type, |                 this.rating = rating | ||||||
|                 mapper.writeValueAsString(data.filter { it != "about:blank" }), |                 setDuration(duration) | ||||||
|                 backgroundPoster, |             } | ||||||
|                 year?.toIntOrNull(), |  | ||||||
|                 descipt, |  | ||||||
|                 null, |  | ||||||
|                 rating, |  | ||||||
|                 duration = duration |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,9 +21,9 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { | ||||||
|     override val hasDownloadSupport = true |     override val hasDownloadSupport = true | ||||||
|     override val usesWebView = true |     override val usesWebView = true | ||||||
|     override val supportedTypes = setOf( |     override val supportedTypes = setOf( | ||||||
|             TvType.Movie, |         TvType.Movie, | ||||||
|             TvType.TvSeries, |         TvType.TvSeries, | ||||||
|         ) |     ) | ||||||
| 
 | 
 | ||||||
|     private fun Element.toSearchResult(): SearchResponse { |     private fun Element.toSearchResult(): SearchResponse { | ||||||
|         val img = this.select("img") |         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/") |             val webViewUrl = "$url${sourceId?.let { ".$it" } ?: ""}".replace("/movie/", "/watch-movie/") | ||||||
| 
 | 
 | ||||||
|             return MovieLoadResponse( |             return newMovieLoadResponse(title, url, TvType.Movie, webViewUrl) { | ||||||
|                 title, |                 this.year = year | ||||||
|                 url, |                 this.posterUrl = posterUrl | ||||||
|                 this.name, |                 this.plot = plot | ||||||
|                 TvType.Movie, |                 setDuration(duration) | ||||||
|                 webViewUrl, |             } | ||||||
|                 posterUrl, |  | ||||||
|                 year, |  | ||||||
|                 plot, |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 duration, |  | ||||||
|                 null, |  | ||||||
|                 null |  | ||||||
|             ) |  | ||||||
|         } else { |         } else { | ||||||
|             val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text |             val seasonsHtml = app.get("$mainUrl/ajax/v2/tv/seasons/$id").text | ||||||
|             val seasonsDocument = Jsoup.parse(seasonsHtml) |             val seasonsDocument = Jsoup.parse(seasonsHtml) | ||||||
|  | @ -212,23 +202,12 @@ class SflixProvider(providerUrl: String, providerName: String) : MainAPI() { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|             return TvSeriesLoadResponse( |             return newTvSeriesLoadResponse(title,url,TvType.TvSeries,episodes) { | ||||||
|                 title, |                 this.posterUrl = posterUrl | ||||||
|                 url, |                 this.year = year | ||||||
|                 this.name, |                 this.plot = plot | ||||||
|                 TvType.TvSeries, |                 setDuration(duration) | ||||||
|                 episodes, |             } | ||||||
|                 posterUrl, |  | ||||||
|                 year, |  | ||||||
|                 plot, |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 null, |  | ||||||
|                 duration, |  | ||||||
|                 null, |  | ||||||
|                 null |  | ||||||
|             ) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,482 +0,0 @@ | ||||||
| package com.lagradost.cloudstream3.movieproviders |  | ||||||
| 
 |  | ||||||
| import com.fasterxml.jackson.annotation.JsonProperty |  | ||||||
| import com.fasterxml.jackson.module.kotlin.readValue |  | ||||||
| import com.lagradost.cloudstream3.* |  | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink |  | ||||||
| import com.lagradost.cloudstream3.utils.getQualityFromName |  | ||||||
| import java.util.concurrent.TimeUnit |  | ||||||
| 
 |  | ||||||
| class ThenosProvider : MainAPI() { |  | ||||||
|     override val mainUrl = "https://www.thenos.org" |  | ||||||
|     override val name = "Thenos" |  | ||||||
|     override val hasQuickSearch = true |  | ||||||
|     override val hasMainPage = true |  | ||||||
|     override val hasChromecastSupport = false |  | ||||||
| 
 |  | ||||||
|     override val supportedTypes = setOf( |  | ||||||
|         TvType.Movie, |  | ||||||
|         TvType.TvSeries, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     override fun getMainPage(): HomePageResponse { |  | ||||||
|         val map = mapOf( |  | ||||||
|             "New Releases" to "released", |  | ||||||
|             "Recently Added in Movies" to "recent", |  | ||||||
|             "Recently Added in Shows" to "recent/shows", |  | ||||||
|             "Top Rated" to "rating" |  | ||||||
|         ) |  | ||||||
|         val list = ArrayList<HomePageList>() |  | ||||||
|         map.entries.forEach { |  | ||||||
|             val url = "$apiUrl/library/${it.value}" |  | ||||||
|             val response = app.get(url).text |  | ||||||
|             val mapped = mapper.readValue<ThenosLoadResponse>(response) |  | ||||||
| 
 |  | ||||||
|             mapped.Metadata?.mapNotNull { meta -> |  | ||||||
|                 meta?.toSearchResponse() |  | ||||||
|             }?.let { searchResponses -> |  | ||||||
|                 list.add( |  | ||||||
|                     HomePageList( |  | ||||||
|                         it.key, |  | ||||||
|                         searchResponses |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return HomePageResponse( |  | ||||||
|             list |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun secondsToReadable(seconds: Int, completedValue: String): String { |  | ||||||
|         var secondsLong = seconds.toLong() |  | ||||||
|         val days = TimeUnit.SECONDS |  | ||||||
|             .toDays(secondsLong) |  | ||||||
|         secondsLong -= TimeUnit.DAYS.toSeconds(days) |  | ||||||
| 
 |  | ||||||
|         val hours = TimeUnit.SECONDS |  | ||||||
|             .toHours(secondsLong) |  | ||||||
|         secondsLong -= TimeUnit.HOURS.toSeconds(hours) |  | ||||||
| 
 |  | ||||||
|         val minutes = TimeUnit.SECONDS |  | ||||||
|             .toMinutes(secondsLong) |  | ||||||
|         secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) |  | ||||||
|         if (minutes < 0) { |  | ||||||
|             return completedValue |  | ||||||
|         } |  | ||||||
|         //println("$days $hours $minutes") |  | ||||||
|         return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private val apiUrl = "https://api.thenos.org" |  | ||||||
| 
 |  | ||||||
|     override fun quickSearch(query: String): List<SearchResponse> { |  | ||||||
|         val url = "$apiUrl/library/search?query=$query" |  | ||||||
|         return searchFromUrl(url) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     data class ThenosSearchResponse( |  | ||||||
|         @JsonProperty("size") val size: Int?, |  | ||||||
|         @JsonProperty("Hub") val Hub: List<Hub>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Part( |  | ||||||
|         @JsonProperty("id") val id: Long?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("duration") val duration: Long?, |  | ||||||
|         @JsonProperty("file") val file: String?, |  | ||||||
|         @JsonProperty("size") val size: Long?, |  | ||||||
|         @JsonProperty("audioProfile") val audioProfile: String?, |  | ||||||
|         @JsonProperty("container") val container: String?, |  | ||||||
|         @JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?, |  | ||||||
|         @JsonProperty("optimizedForStreaming") val optimizedForStreaming: Boolean?, |  | ||||||
|         @JsonProperty("videoProfile") val videoProfile: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Media( |  | ||||||
|         @JsonProperty("id") val id: Long?, |  | ||||||
|         @JsonProperty("duration") val duration: Long?, |  | ||||||
|         @JsonProperty("bitrate") val bitrate: Long?, |  | ||||||
|         @JsonProperty("width") val width: Long?, |  | ||||||
|         @JsonProperty("height") val height: Long?, |  | ||||||
|         @JsonProperty("aspectRatio") val aspectRatio: Double?, |  | ||||||
|         @JsonProperty("audioChannels") val audioChannels: Long?, |  | ||||||
|         @JsonProperty("audioCodec") val audioCodec: String?, |  | ||||||
|         @JsonProperty("videoCodec") val videoCodec: String?, |  | ||||||
|         @JsonProperty("videoResolution") val videoResolution: String?, |  | ||||||
|         @JsonProperty("container") val container: String?, |  | ||||||
|         @JsonProperty("videoFrameRate") val videoFrameRate: String?, |  | ||||||
|         @JsonProperty("optimizedForStreaming") val optimizedForStreaming: Long?, |  | ||||||
|         @JsonProperty("audioProfile") val audioProfile: String?, |  | ||||||
|         @JsonProperty("has64bitOffsets") val has64bitOffsets: Boolean?, |  | ||||||
|         @JsonProperty("videoProfile") val videoProfile: String?, |  | ||||||
|         @JsonProperty("Part") val Part: List<Part>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Genre( |  | ||||||
|         @JsonProperty("tag") val tag: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     data class Country( |  | ||||||
|         @JsonProperty("tag") val tag: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     data class Role( |  | ||||||
|         @JsonProperty("tag") val tag: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Hub( |  | ||||||
|         @JsonProperty("title") val title: String?, |  | ||||||
|         @JsonProperty("type") val type: String?, |  | ||||||
|         @JsonProperty("hubIdentifier") val hubIdentifier: String?, |  | ||||||
|         @JsonProperty("context") val context: String?, |  | ||||||
|         @JsonProperty("size") val size: Int?, |  | ||||||
|         @JsonProperty("more") val more: Boolean?, |  | ||||||
|         @JsonProperty("style") val style: String?, |  | ||||||
|         @JsonProperty("Metadata") val Metadata: List<Metadata>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Metadata( |  | ||||||
|         @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, |  | ||||||
|         @JsonProperty("ratingKey") val ratingKey: String?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("guid") val guid: String?, |  | ||||||
|         @JsonProperty("studio") val studio: String?, |  | ||||||
|         @JsonProperty("type") val type: String?, |  | ||||||
|         @JsonProperty("title") val title: String?, |  | ||||||
|         @JsonProperty("librarySectionID") val librarySectionID: Int?, |  | ||||||
|         @JsonProperty("librarySectionKey") val librarySectionKey: String?, |  | ||||||
|         @JsonProperty("contentRating") val contentRating: String?, |  | ||||||
|         @JsonProperty("summary") val summary: String?, |  | ||||||
|         @JsonProperty("audienceRating") val audienceRating: Int?, |  | ||||||
|         @JsonProperty("year") val year: Int?, |  | ||||||
|         @JsonProperty("thumb") val thumb: String?, |  | ||||||
|         @JsonProperty("art") val art: String?, |  | ||||||
|         @JsonProperty("duration") val duration: Int?, |  | ||||||
|         @JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?, |  | ||||||
|         @JsonProperty("addedAt") val addedAt: Int?, |  | ||||||
|         @JsonProperty("updatedAt") val updatedAt: Int?, |  | ||||||
|         @JsonProperty("audienceRatingImage") val audienceRatingImage: String?, |  | ||||||
|         @JsonProperty("Media") val Media: List<Media>?, |  | ||||||
|         @JsonProperty("Genre") val Genre: List<Genre>?, |  | ||||||
|         @JsonProperty("Director") val Director: List<Director>?, |  | ||||||
|         @JsonProperty("Country") val Country: List<Country>?, |  | ||||||
|         @JsonProperty("Role") val Role: List<Role>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Director( |  | ||||||
|         @JsonProperty("tag") val tag: String |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     private fun Metadata.toSearchResponse(): SearchResponse? { |  | ||||||
|         if (type == "movie") { |  | ||||||
|             return MovieSearchResponse( |  | ||||||
|                 title ?: "", |  | ||||||
|                 ratingKey ?: return null, |  | ||||||
|                 this@ThenosProvider.name, |  | ||||||
|                 TvType.Movie, |  | ||||||
|                 art?.let { "$apiUrl$it" }, |  | ||||||
|                 year |  | ||||||
| 
 |  | ||||||
|             ) |  | ||||||
|         } else if (type == "show") { |  | ||||||
|             return TvSeriesSearchResponse( |  | ||||||
|                 title ?: "", |  | ||||||
|                 ratingKey ?: return null, |  | ||||||
|                 this@ThenosProvider.name, |  | ||||||
|                 TvType.TvSeries, |  | ||||||
|                 art?.let { "$apiUrl$it" }, |  | ||||||
|                 year, |  | ||||||
|                 null |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         return null |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun searchFromUrl(url: String): List<SearchResponse> { |  | ||||||
|         val response = app.get(url).text |  | ||||||
|         val test = mapper.readValue<ThenosSearchResponse>(response) |  | ||||||
|         val returnValue = ArrayList<SearchResponse>() |  | ||||||
| 
 |  | ||||||
|         test.Hub?.forEach { |  | ||||||
|             it.Metadata?.forEach metadata@{ meta -> |  | ||||||
|                 if (meta.ratingKey == null || meta.title == null) return@metadata |  | ||||||
|                 meta.toSearchResponse()?.let { response -> returnValue.add(response) } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return returnValue |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun search(query: String): List<SearchResponse> { |  | ||||||
|         val url = "$apiUrl/library/search/advance?query=$query" |  | ||||||
|         return searchFromUrl(url) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     data class ThenosSource( |  | ||||||
|         @JsonProperty("title") val title: String?, |  | ||||||
|         @JsonProperty("image") val image: String?, |  | ||||||
|         @JsonProperty("sources") val sources: List<Sources>?, |  | ||||||
|         @JsonProperty("tracks") val tracks: List<Tracks> |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Sources( |  | ||||||
|         @JsonProperty("file") val file: String?, |  | ||||||
|         @JsonProperty("label") val label: String?, |  | ||||||
|         @JsonProperty("default") val default: Boolean?, |  | ||||||
|         @JsonProperty("type") val type: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class Tracks( |  | ||||||
|         @JsonProperty("file") val file: String?, |  | ||||||
|         @JsonProperty("label") val label: String?, |  | ||||||
|         @JsonProperty("kind") val kind: String? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     override fun loadLinks( |  | ||||||
|         data: String, |  | ||||||
|         isCasting: Boolean, |  | ||||||
|         subtitleCallback: (SubtitleFile) -> Unit, |  | ||||||
|         callback: (ExtractorLink) -> Unit |  | ||||||
|     ): Boolean { |  | ||||||
|         val url = "$apiUrl/library/watch/$data" |  | ||||||
|         val response = app.get(url).text |  | ||||||
|         val mapped = mapper.readValue<ThenosSource>(response) |  | ||||||
| 
 |  | ||||||
|         mapped.sources?.forEach { source -> |  | ||||||
|             val isM3u8 = source.type != "video/mp4" |  | ||||||
|             val token = app.get("https://token.noss.workers.dev/").text |  | ||||||
|             val authorization = |  | ||||||
|                 base64Decode(token) |  | ||||||
| 
 |  | ||||||
|             callback.invoke( |  | ||||||
|                 ExtractorLink( |  | ||||||
|                     this.name, |  | ||||||
|                     "${this.name} ${source.label ?: ""}", |  | ||||||
|                     (source.file)?.split("/")?.lastOrNull()?.let { |  | ||||||
|                         "https://www.googleapis.com/drive/v3/files/$it?alt=media" |  | ||||||
|                     } ?: return@forEach, |  | ||||||
|                     "https://www.thenos.org/", |  | ||||||
|                     getQualityFromName(source.label ?: ""), |  | ||||||
|                     isM3u8, |  | ||||||
|                     mapOf("authorization" to "Bearer $authorization") |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         mapped.tracks.forEach { |  | ||||||
|             subtitleCallback.invoke( |  | ||||||
|                 SubtitleFile( |  | ||||||
|                     it.label ?: "English", |  | ||||||
|                     it.file ?: return@forEach |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     data class ThenosLoadResponse( |  | ||||||
|         @JsonProperty("size") val size: Long?, |  | ||||||
|         @JsonProperty("allowSync") val allowSync: Boolean?, |  | ||||||
|         @JsonProperty("augmentationKey") val augmentationKey: String?, |  | ||||||
|         @JsonProperty("identifier") val identifier: String?, |  | ||||||
|         @JsonProperty("librarySectionID") val librarySectionID: Long?, |  | ||||||
|         @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, |  | ||||||
|         @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, |  | ||||||
|         @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, |  | ||||||
|         @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, |  | ||||||
|         @JsonProperty("Metadata") val Metadata: List<Metadata?>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     data class ThenosSeriesResponse( |  | ||||||
|         @JsonProperty("size") val size: Long?, |  | ||||||
|         @JsonProperty("allowSync") val allowSync: Boolean?, |  | ||||||
|         @JsonProperty("art") val art: String?, |  | ||||||
|         @JsonProperty("identifier") val identifier: String?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("librarySectionID") val librarySectionID: Long?, |  | ||||||
|         @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, |  | ||||||
|         @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, |  | ||||||
|         @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, |  | ||||||
|         @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, |  | ||||||
|         @JsonProperty("nocache") val nocache: Boolean?, |  | ||||||
|         @JsonProperty("parentIndex") val parentIndex: Long?, |  | ||||||
|         @JsonProperty("parentTitle") val parentTitle: String?, |  | ||||||
|         @JsonProperty("parentYear") val parentYear: Long?, |  | ||||||
|         @JsonProperty("summary") val summary: String?, |  | ||||||
|         @JsonProperty("theme") val theme: String?, |  | ||||||
|         @JsonProperty("thumb") val thumb: String?, |  | ||||||
|         @JsonProperty("title1") val title1: String?, |  | ||||||
|         @JsonProperty("title2") val title2: String?, |  | ||||||
|         @JsonProperty("viewGroup") val viewGroup: String?, |  | ||||||
|         @JsonProperty("viewMode") val viewMode: Long?, |  | ||||||
|         @JsonProperty("Metadata") val Metadata: List<SeriesMetadata>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class SeriesMetadata( |  | ||||||
|         @JsonProperty("ratingKey") val ratingKey: String?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("parentRatingKey") val parentRatingKey: String?, |  | ||||||
|         @JsonProperty("guid") val guid: String?, |  | ||||||
|         @JsonProperty("parentGuid") val parentGuid: String?, |  | ||||||
|         @JsonProperty("parentStudio") val parentStudio: String?, |  | ||||||
|         @JsonProperty("type") val type: String?, |  | ||||||
|         @JsonProperty("title") val title: String?, |  | ||||||
|         @JsonProperty("parentKey") val parentKey: String?, |  | ||||||
|         @JsonProperty("parentTitle") val parentTitle: String?, |  | ||||||
|         @JsonProperty("summary") val summary: String?, |  | ||||||
|         @JsonProperty("index") val index: Long?, |  | ||||||
|         @JsonProperty("parentIndex") val parentIndex: Long?, |  | ||||||
|         @JsonProperty("parentYear") val parentYear: Long?, |  | ||||||
|         @JsonProperty("thumb") val thumb: String?, |  | ||||||
|         @JsonProperty("art") val art: String?, |  | ||||||
|         @JsonProperty("parentThumb") val parentThumb: String?, |  | ||||||
|         @JsonProperty("parentTheme") val parentTheme: String?, |  | ||||||
|         @JsonProperty("leafCount") val leafCount: Long?, |  | ||||||
|         @JsonProperty("viewedLeafCount") val viewedLeafCount: Long?, |  | ||||||
|         @JsonProperty("addedAt") val addedAt: Long?, |  | ||||||
|         @JsonProperty("updatedAt") val updatedAt: Int? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class SeasonResponse( |  | ||||||
|         @JsonProperty("size") val size: Long?, |  | ||||||
|         @JsonProperty("allowSync") val allowSync: Boolean?, |  | ||||||
|         @JsonProperty("art") val art: String?, |  | ||||||
|         @JsonProperty("grandparentContentRating") val grandparentContentRating: String?, |  | ||||||
|         @JsonProperty("grandparentRatingKey") val grandparentRatingKey: Long?, |  | ||||||
|         @JsonProperty("grandparentStudio") val grandparentStudio: String?, |  | ||||||
|         @JsonProperty("grandparentTheme") val grandparentTheme: String?, |  | ||||||
|         @JsonProperty("grandparentThumb") val grandparentThumb: String?, |  | ||||||
|         @JsonProperty("grandparentTitle") val grandparentTitle: String?, |  | ||||||
|         @JsonProperty("identifier") val identifier: String?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("librarySectionID") val librarySectionID: Long?, |  | ||||||
|         @JsonProperty("librarySectionTitle") val librarySectionTitle: String?, |  | ||||||
|         @JsonProperty("librarySectionUUID") val librarySectionUUID: String?, |  | ||||||
|         @JsonProperty("mediaTagPrefix") val mediaTagPrefix: String?, |  | ||||||
|         @JsonProperty("mediaTagVersion") val mediaTagVersion: Long?, |  | ||||||
|         @JsonProperty("nocache") val nocache: Boolean?, |  | ||||||
|         @JsonProperty("parentIndex") val parentIndex: Long?, |  | ||||||
|         @JsonProperty("parentTitle") val parentTitle: String?, |  | ||||||
|         @JsonProperty("summary") val summary: String?, |  | ||||||
|         @JsonProperty("theme") val theme: String?, |  | ||||||
|         @JsonProperty("thumb") val thumb: String?, |  | ||||||
|         @JsonProperty("title1") val title1: String?, |  | ||||||
|         @JsonProperty("title2") val title2: String?, |  | ||||||
|         @JsonProperty("viewGroup") val viewGroup: String?, |  | ||||||
|         @JsonProperty("viewMode") val viewMode: Long?, |  | ||||||
|         @JsonProperty("Metadata") val Metadata: List<SeasonMetadata>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     data class SeasonMetadata( |  | ||||||
|         @JsonProperty("ratingKey") val ratingKey: String?, |  | ||||||
|         @JsonProperty("key") val key: String?, |  | ||||||
|         @JsonProperty("parentRatingKey") val parentRatingKey: String?, |  | ||||||
|         @JsonProperty("grandparentRatingKey") val grandparentRatingKey: String?, |  | ||||||
|         @JsonProperty("guid") val guid: String?, |  | ||||||
|         @JsonProperty("parentGuid") val parentGuid: String?, |  | ||||||
|         @JsonProperty("grandparentGuid") val grandparentGuid: String?, |  | ||||||
|         @JsonProperty("type") val type: String?, |  | ||||||
|         @JsonProperty("title") val title: String?, |  | ||||||
|         @JsonProperty("grandparentKey") val grandparentKey: String?, |  | ||||||
|         @JsonProperty("parentKey") val parentKey: String?, |  | ||||||
|         @JsonProperty("grandparentTitle") val grandparentTitle: String?, |  | ||||||
|         @JsonProperty("parentTitle") val parentTitle: String?, |  | ||||||
|         @JsonProperty("contentRating") val contentRating: String?, |  | ||||||
|         @JsonProperty("summary") val summary: String?, |  | ||||||
|         @JsonProperty("index") val index: Int?, |  | ||||||
|         @JsonProperty("parentIndex") val parentIndex: Int?, |  | ||||||
|         @JsonProperty("audienceRating") val audienceRating: Double?, |  | ||||||
|         @JsonProperty("thumb") val thumb: String?, |  | ||||||
|         @JsonProperty("art") val art: String?, |  | ||||||
|         @JsonProperty("parentThumb") val parentThumb: String?, |  | ||||||
|         @JsonProperty("grandparentThumb") val grandparentThumb: String?, |  | ||||||
|         @JsonProperty("grandparentArt") val grandparentArt: String?, |  | ||||||
|         @JsonProperty("grandparentTheme") val grandparentTheme: String?, |  | ||||||
|         @JsonProperty("duration") val duration: Long?, |  | ||||||
|         @JsonProperty("originallyAvailableAt") val originallyAvailableAt: String?, |  | ||||||
|         @JsonProperty("addedAt") val addedAt: Long?, |  | ||||||
|         @JsonProperty("updatedAt") val updatedAt: Long?, |  | ||||||
|         @JsonProperty("audienceRatingImage") val audienceRatingImage: String?, |  | ||||||
|         @JsonProperty("Media") val Media: List<Media>?, |  | ||||||
|         @JsonProperty("Director") val Director: List<Director>?, |  | ||||||
|         @JsonProperty("Role") val Role: List<Role>? |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     private fun getAllEpisodes(id: String): List<TvSeriesEpisode> { |  | ||||||
|         val episodes = ArrayList<TvSeriesEpisode>() |  | ||||||
|         val url = "$apiUrl/library/metadata/$id/children" |  | ||||||
|         val response = app.get(url).text |  | ||||||
|         val mapped = mapper.readValue<ThenosSeriesResponse>(response) |  | ||||||
|         mapped.Metadata?.forEach { series_meta -> |  | ||||||
|             val fixedUrl = apiUrl + series_meta.key |  | ||||||
|             val child = app.get(fixedUrl).text |  | ||||||
|             val mappedSeason = mapper.readValue<SeasonResponse>(child) |  | ||||||
|             mappedSeason.Metadata?.forEach mappedSeason@{ meta -> |  | ||||||
|                 episodes.add( |  | ||||||
|                     TvSeriesEpisode( |  | ||||||
|                         meta.title, |  | ||||||
|                         meta.parentIndex, |  | ||||||
|                         meta.index, |  | ||||||
|                         meta.ratingKey ?: return@mappedSeason, |  | ||||||
|                         meta.thumb?.let { "$apiUrl$it" }, |  | ||||||
|                         meta.originallyAvailableAt, |  | ||||||
|                         (meta.audienceRating?.times(10))?.toInt(), |  | ||||||
|                         meta.summary |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|         } |  | ||||||
|         return episodes |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun load(url: String): LoadResponse? { |  | ||||||
|         val fixedUrl = "$apiUrl/library/metadata/${url.split("/").last()}" |  | ||||||
|         val response = app.get(fixedUrl).text |  | ||||||
|         val mapped = mapper.readValue<ThenosLoadResponse>(response) |  | ||||||
| 
 |  | ||||||
|         val isShow = mapped.Metadata?.any { it?.type == "show" } == true |  | ||||||
|         val metadata = mapped.Metadata?.getOrNull(0) ?: return null |  | ||||||
| 
 |  | ||||||
|         return if (!isShow) { |  | ||||||
|             MovieLoadResponse( |  | ||||||
|                 metadata.title ?: "No title found", |  | ||||||
|                 "$mainUrl/movie/${metadata.ratingKey}", |  | ||||||
|                 this.name, |  | ||||||
|                 TvType.Movie, |  | ||||||
|                 metadata.ratingKey ?: return null, |  | ||||||
|                 metadata.art?.let { "$apiUrl$it" }, |  | ||||||
|                 metadata.year, |  | ||||||
|                 metadata.summary, |  | ||||||
|                 null, // with Guid this is possible |  | ||||||
|                 metadata.audienceRating?.times(10), |  | ||||||
|                 metadata.Genre?.mapNotNull { it.tag }, |  | ||||||
|                 metadata.duration?.let { secondsToReadable(it / 1000, "") }, |  | ||||||
|                 null |  | ||||||
|             ) |  | ||||||
|         } else { |  | ||||||
|             TvSeriesLoadResponse( |  | ||||||
|                 metadata.title ?: "No title found", |  | ||||||
|                 "$mainUrl/show/${metadata.ratingKey}", |  | ||||||
|                 this.name, |  | ||||||
|                 TvType.TvSeries, |  | ||||||
|                 metadata.ratingKey?.let { getAllEpisodes(it) } ?: return null, |  | ||||||
|                 metadata.art?.let { "$apiUrl$it" }, |  | ||||||
|                 metadata.year, |  | ||||||
|                 metadata.summary, |  | ||||||
|                 null, // with Guid this is possible |  | ||||||
|                 null,// with Guid this is possible |  | ||||||
|                 metadata.audienceRating?.times(10), |  | ||||||
|                 metadata.Genre?.mapNotNull { it.tag }, |  | ||||||
|                 metadata.duration?.let { secondsToReadable(it / 1000, "") }, |  | ||||||
|                 null |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,305 +0,0 @@ | ||||||
| package com.lagradost.cloudstream3.movieproviders |  | ||||||
| 
 |  | ||||||
| import com.fasterxml.jackson.module.kotlin.readValue |  | ||||||
| import com.lagradost.cloudstream3.* |  | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink |  | ||||||
| import com.lagradost.cloudstream3.utils.Qualities |  | ||||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
| 
 |  | ||||||
| // referer = https://trailers.to, USERAGENT ALSO REQUIRED |  | ||||||
| class TrailersToProvider : MainAPI() { |  | ||||||
|     override val mainUrl = "https://trailers.to" |  | ||||||
|     override val name = "Trailers.to" |  | ||||||
|     override val hasQuickSearch = true |  | ||||||
|     override val hasMainPage = true |  | ||||||
|     override val hasChromecastSupport = false |  | ||||||
|     override val supportedTypes = setOf( |  | ||||||
|         TvType.Movie, |  | ||||||
|         TvType.TvSeries, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     override val vpnStatus = VPNStatus.MightBeNeeded |  | ||||||
| 
 |  | ||||||
|     override fun getMainPage(): HomePageResponse? { |  | ||||||
|         val response = app.get(mainUrl).text |  | ||||||
|         val document = Jsoup.parse(response) |  | ||||||
|         val returnList = ArrayList<HomePageList>() |  | ||||||
|         val docs = document.select("section.section > div.container") |  | ||||||
|         for (doc in docs) { |  | ||||||
|             val epList = doc.selectFirst("> div.owl-carousel") ?: continue |  | ||||||
|             val title = doc.selectFirst("> div.text-center > h2").text() |  | ||||||
|             val list = epList.select("> div.item > div.box-nina") |  | ||||||
|             val isMovieType = title.contains("Movie") |  | ||||||
|             val currentList = list.mapNotNull { head -> |  | ||||||
|                 val hrefItem = head.selectFirst("> div.box-nina-media > a") |  | ||||||
|                 val href = fixUrl(hrefItem.attr("href")) |  | ||||||
|                 val img = hrefItem.selectFirst("> img") |  | ||||||
|                 val posterUrl = img.attr("src") |  | ||||||
|                 val name = img.attr("alt") |  | ||||||
|                 return@mapNotNull if (isMovieType) MovieSearchResponse( |  | ||||||
|                     name, |  | ||||||
|                     href, |  | ||||||
|                     this.name, |  | ||||||
|                     TvType.Movie, |  | ||||||
|                     posterUrl, |  | ||||||
|                     null |  | ||||||
|                 ) else TvSeriesSearchResponse( |  | ||||||
|                     name, |  | ||||||
|                     href, |  | ||||||
|                     this.name, |  | ||||||
|                     TvType.TvSeries, |  | ||||||
|                     posterUrl, |  | ||||||
|                     null, null |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|             if (currentList.isNotEmpty()) { |  | ||||||
|                 returnList.add(HomePageList(title, currentList)) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (returnList.size <= 0) return null |  | ||||||
| 
 |  | ||||||
|         return HomePageResponse(returnList) |  | ||||||
|         //section.section > div.container > div.owl-carousel |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun quickSearch(query: String): List<SearchResponse> { |  | ||||||
|         val url = "$mainUrl/en/quick-search?q=$query" |  | ||||||
|         val response = app.get(url).text |  | ||||||
|         val document = Jsoup.parse(response) |  | ||||||
|         val items = document.select("div.group-post-minimal > a.post-minimal") |  | ||||||
|         if (items.isNullOrEmpty()) return ArrayList() |  | ||||||
| 
 |  | ||||||
|         val returnValue = ArrayList<SearchResponse>() |  | ||||||
|         for (item in items) { |  | ||||||
|             val href = fixUrl(item.attr("href")) |  | ||||||
|             val poster = item.selectFirst("> div.post-minimal-media > img").attr("src") |  | ||||||
|             val header = item.selectFirst("> div.post-minimal-main") |  | ||||||
|             val name = header.selectFirst("> span.link-black").text() |  | ||||||
|             val info = header.select("> p") |  | ||||||
|             val year = info?.get(1)?.text()?.toIntOrNull() |  | ||||||
|             val isTvShow = href.contains("/tvshow/") |  | ||||||
| 
 |  | ||||||
|             returnValue.add( |  | ||||||
|                 if (isTvShow) { |  | ||||||
|                     TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null) |  | ||||||
|                 } else { |  | ||||||
|                     MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year) |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         return returnValue |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun search(query: String): List<SearchResponse> { |  | ||||||
|         val url = "$mainUrl/en/popular/movies-tvshows-collections?q=$query" |  | ||||||
|         val response = app.get(url).text |  | ||||||
|         val document = Jsoup.parse(response) |  | ||||||
|         val items = document.select("div.col-lg-8 > article.list-item") |  | ||||||
|         if (items.isNullOrEmpty()) return ArrayList() |  | ||||||
|         val returnValue = ArrayList<SearchResponse>() |  | ||||||
|         for (item in items) { |  | ||||||
|             val poster = item.selectFirst("> div.tour-modern-media > a.tour-modern-figure > img").attr("src") |  | ||||||
|             val infoDiv = item.selectFirst("> div.tour-modern-main") |  | ||||||
|             val nameHeader = infoDiv.select("> h5.tour-modern-title > a").last() |  | ||||||
|             val name = nameHeader.text() |  | ||||||
|             val href = fixUrl(nameHeader.attr("href")) |  | ||||||
|             val year = infoDiv.selectFirst("> div > span.small-text")?.text()?.takeLast(4)?.toIntOrNull() |  | ||||||
|             val isTvShow = href.contains("/tvshow/") |  | ||||||
| 
 |  | ||||||
|             returnValue.add( |  | ||||||
|                 if (isTvShow) { |  | ||||||
|                     TvSeriesSearchResponse(name, href, this.name, TvType.TvSeries, poster, year, null) |  | ||||||
|                 } else { |  | ||||||
|                     MovieSearchResponse(name, href, this.name, TvType.Movie, poster, year) |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         return returnValue |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun loadLink( |  | ||||||
|         data: String, |  | ||||||
|         callback: (ExtractorLink) -> Unit, |  | ||||||
|     ): Boolean { |  | ||||||
|         val response = app.get(data).text |  | ||||||
|         val url = "<source src='(.*?)'".toRegex().find(response)?.groupValues?.get(1) |  | ||||||
|         if (url != null) { |  | ||||||
|             callback.invoke(ExtractorLink(this.name, this.name, url, mainUrl, Qualities.Unknown.value, false)) |  | ||||||
|         } |  | ||||||
|         return url != null |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun loadSubs(url: String, subtitleCallback: (SubtitleFile) -> Unit) { |  | ||||||
|         if (url.isEmpty()) return |  | ||||||
| 
 |  | ||||||
|         val response = app.get(fixUrl(url)).text |  | ||||||
|         val document = Jsoup.parse(response) |  | ||||||
| 
 |  | ||||||
|         val items = document.select("div.list-group > a.list-group-item") |  | ||||||
|         for (item in items) { |  | ||||||
|             val hash = item.attr("hash") ?: continue |  | ||||||
|             val languageCode = item.attr("languagecode") ?: continue |  | ||||||
|             if (hash.isEmpty()) continue |  | ||||||
|             if (languageCode.isEmpty()) continue |  | ||||||
| 
 |  | ||||||
|             subtitleCallback.invoke( |  | ||||||
|                 SubtitleFile( |  | ||||||
|                     SubtitleHelper.fromTwoLettersToLanguage(languageCode) ?: languageCode, |  | ||||||
|                     "$mainUrl/subtitles/$hash" |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun loadLinks( |  | ||||||
|         data: String, |  | ||||||
|         isCasting: Boolean, |  | ||||||
|         subtitleCallback: (SubtitleFile) -> Unit, |  | ||||||
|         callback: (ExtractorLink) -> Unit |  | ||||||
|     ): Boolean { |  | ||||||
|         if (isCasting) return false |  | ||||||
|         val pairData = mapper.readValue<Pair<String, String>>(data) |  | ||||||
|         val url = pairData.second |  | ||||||
| 
 |  | ||||||
|         val isMovie = url.contains("/web-sources/") |  | ||||||
|         if (isMovie) { |  | ||||||
|             val isSucc = loadLink(url, callback) |  | ||||||
|             val subUrl = pairData.first |  | ||||||
|             loadSubs(subUrl, subtitleCallback) |  | ||||||
| 
 |  | ||||||
|             return isSucc |  | ||||||
|         } else if (url.contains("/episode/")) { |  | ||||||
|             val response = app.get(url, params = mapOf("preview" to "1")).text |  | ||||||
|             val document = Jsoup.parse(response) |  | ||||||
|             // val qSub = document.select("subtitle-content") |  | ||||||
|             val subUrl = document.select("subtitle-content")?.attr("data-url") ?: "" |  | ||||||
| 
 |  | ||||||
|             val subData = fixUrl(document.selectFirst("content").attr("data-url") ?: return false) |  | ||||||
|             val isSucc = if (subData.contains("/web-sources/")) { |  | ||||||
|                 loadLink(subData, callback) |  | ||||||
|             } else false |  | ||||||
| 
 |  | ||||||
|             loadSubs(subUrl, subtitleCallback) |  | ||||||
|             return isSucc |  | ||||||
|         } |  | ||||||
|         return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun load(url: String): LoadResponse { |  | ||||||
|         val response = app.get(if (url.endsWith("?preview=1")) url else "$url?preview=1").text |  | ||||||
|         val document = Jsoup.parse(response) |  | ||||||
|         var title = document?.selectFirst("h2.breadcrumbs-custom-title > a")?.text() |  | ||||||
|             ?: throw ErrorLoadingException("Service might be unavailable") |  | ||||||
| 
 |  | ||||||
|         val metaInfo = document.select("div.post-info-meta > ul.post-info-meta-list > li") |  | ||||||
|         val year = metaInfo?.get(0)?.selectFirst("> span.small-text")?.text()?.takeLast(4)?.toIntOrNull() |  | ||||||
|         val rating = parseRating(metaInfo?.get(1)?.selectFirst("> span.small-text")?.text()?.replace("/ 10", "")) |  | ||||||
|         val duration = metaInfo?.get(2)?.selectFirst("> span.small-text")?.text() |  | ||||||
|         val imdbUrl = metaInfo?.get(3)?.selectFirst("> a")?.attr("href") |  | ||||||
|         val trailer = metaInfo?.get(4)?.selectFirst("> a")?.attr("href") |  | ||||||
|         val poster = document.selectFirst("div.slider-image > a > img").attr("src") |  | ||||||
|         val descriptHeader = document.selectFirst("article.post-info") |  | ||||||
|         title = title.substring(0, title.length - 6) // REMOVE YEAR |  | ||||||
| 
 |  | ||||||
|         val descript = descriptHeader.select("> div > p").text() |  | ||||||
|         val table = descriptHeader.select("> table.post-info-table > tbody > tr > td") |  | ||||||
|         var generes: List<String>? = null |  | ||||||
|         for (i in 0 until table.size / 2) { |  | ||||||
|             val header = table[i * 2].text() |  | ||||||
|             val info = table[i * 2 + 1] |  | ||||||
|             when (header) { |  | ||||||
|                 "Genre" -> generes = info.text().split(",") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         val tags = if (generes == null) null else ArrayList(generes) |  | ||||||
| 
 |  | ||||||
|         val isTvShow = url.contains("/tvshow/") |  | ||||||
|         if (isTvShow) { |  | ||||||
|             val episodes = document.select("#seasons-accordion .card-body > .tour-modern") |  | ||||||
|                 ?: throw ErrorLoadingException("No Episodes found") |  | ||||||
|             val parsedEpisodes = episodes.withIndex().map { (index, item) -> |  | ||||||
|                 val epPoster = item.selectFirst("img").attr("src") |  | ||||||
|                 val main = item.selectFirst(".tour-modern-main") |  | ||||||
|                 val titleHeader = main.selectFirst("a") |  | ||||||
|                 val titleName = titleHeader.text() |  | ||||||
|                 val href = fixUrl(titleHeader.attr("href")) |  | ||||||
|                 val gValues = |  | ||||||
|                     Regex(""".*?[\w\s]+ ([0-9]+)(?::[\w\s]+)?\s-\s(?:Episode )?([0-9]+)?(?:: )?(.*)""").find(titleName)?.destructured |  | ||||||
|                 val season = gValues?.component1()?.toIntOrNull() |  | ||||||
|                 var episode = gValues?.component2()?.toIntOrNull() |  | ||||||
|                 if (episode == null) { |  | ||||||
|                     episode = index + 1 |  | ||||||
|                 } |  | ||||||
|                 val epName = |  | ||||||
|                     if (gValues?.component3()?.isNotEmpty() == true) gValues.component3() else "Episode $episode" |  | ||||||
|                 val infoHeaders = main.select("span.small-text") |  | ||||||
|                 val date = infoHeaders?.get(0)?.text() |  | ||||||
|                 val ratingText = infoHeaders?.get(1)?.text()?.replace("/ 10", "") |  | ||||||
|                 val epRating = if (ratingText == null) null else parseRating(ratingText) |  | ||||||
|                 val epDescript = main.selectFirst("p")?.text() |  | ||||||
| 
 |  | ||||||
|                 TvSeriesEpisode( |  | ||||||
|                     epName, |  | ||||||
|                     season, |  | ||||||
|                     episode, |  | ||||||
|                     mapper.writeValueAsString(Pair("", href)), |  | ||||||
|                     epPoster, |  | ||||||
|                     date, |  | ||||||
|                     epRating, |  | ||||||
|                     epDescript |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|             return TvSeriesLoadResponse( |  | ||||||
|                 title, |  | ||||||
|                 url, |  | ||||||
|                 this.name, |  | ||||||
|                 TvType.TvSeries, |  | ||||||
|                 ArrayList(parsedEpisodes), |  | ||||||
|                 poster, |  | ||||||
|                 year, |  | ||||||
|                 descript, |  | ||||||
|                 null, |  | ||||||
|                 imdbUrlToIdNullable(imdbUrl), |  | ||||||
|                 rating, |  | ||||||
|                 tags, |  | ||||||
|                 duration, |  | ||||||
|                 trailer |  | ||||||
|             ) |  | ||||||
|         } else { |  | ||||||
| 
 |  | ||||||
|             //https://trailers.to/en/subtitle-details/2086212/jungle-cruise-2021?imdbId=tt0870154&season=0&episode=0 |  | ||||||
|             //https://trailers.to/en/movie/2086212/jungle-cruise-2021 |  | ||||||
| 
 |  | ||||||
|             val subUrl = if (imdbUrl != null) { |  | ||||||
|                 val imdbId = imdbUrlToId(imdbUrl) |  | ||||||
|                 url.replace("/movie/", "/subtitle-details/") + "?imdbId=$imdbId&season=0&episode=0" |  | ||||||
|             } else "" |  | ||||||
| 
 |  | ||||||
|             val data = mapper.writeValueAsString( |  | ||||||
|                 Pair( |  | ||||||
|                     subUrl, |  | ||||||
|                     fixUrl( |  | ||||||
|                         document.selectFirst("content")?.attr("data-url") |  | ||||||
|                             ?: throw ErrorLoadingException("Link not found") |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|             return MovieLoadResponse( |  | ||||||
|                 title, |  | ||||||
|                 url, |  | ||||||
|                 this.name, |  | ||||||
|                 TvType.Movie, |  | ||||||
|                 data, |  | ||||||
|                 poster, |  | ||||||
|                 year, |  | ||||||
|                 descript, |  | ||||||
|                 imdbUrlToIdNullable(imdbUrl), |  | ||||||
|                 rating, |  | ||||||
|                 tags, |  | ||||||
|                 duration, |  | ||||||
|                 trailer |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -44,11 +44,11 @@ class VfSerieProvider : MainAPI() { | ||||||
| 
 | 
 | ||||||
|     private fun getDirect(original: String): String {  // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example |     private fun getDirect(original: String): String {  // original data, https://vf-serie.org/?trembed=1&trid=80467&trtype=2 for example | ||||||
|         val response = app.get(original).text |         val response = app.get(original).text | ||||||
|         val url = "iframe .*src=\\\"(.*?)\\\"".toRegex().find(response)?.groupValues?.get(1) |         val url = "iframe .*src=\"(.*?)\"".toRegex().find(response)?.groupValues?.get(1) | ||||||
|             .toString()  // https://vudeo.net/embed-7jdb1t5b2mvo.html for example |             .toString()  // https://vudeo.net/embed-7jdb1t5b2mvo.html for example | ||||||
|         val vudoResponse = app.get(url).text |         val vudoResponse = app.get(url).text | ||||||
|         val document = Jsoup.parse(vudoResponse) |         val document = Jsoup.parse(vudoResponse) | ||||||
|         return Regex("sources: \\[\"(.*?)\"\\]").find(document.html())?.groupValues?.get(1) |         return Regex("sources: \\[\"(.*?)\"]").find(document.html())?.groupValues?.get(1) | ||||||
|             .toString()  // direct mp4 link, https://m5.vudeo.net/2vp3xgpw2avjdohilpfbtyuxzzrqzuh4z5yxvztral5k3rjnba6f4byj3saa/v.mp4 for exemple |             .toString()  // direct mp4 link, https://m5.vudeo.net/2vp3xgpw2avjdohilpfbtyuxzzrqzuh4z5yxvztral5k3rjnba6f4byj3saa/v.mp4 for exemple | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,12 +6,6 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKeys | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||||
| 
 | 
 | ||||||
| abstract class AccountManager(private val defIndex: Int) : OAuth2API { | abstract class AccountManager(private val defIndex: Int) : OAuth2API { | ||||||
|     // don't change this as all keys depend on it |  | ||||||
|     open val idPrefix: String |  | ||||||
|         get() { |  | ||||||
|             throw(NotImplementedError()) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     var accountIndex = defIndex |     var accountIndex = defIndex | ||||||
|     protected val accountId get() = "${idPrefix}_account_$accountIndex" |     protected val accountId get() = "${idPrefix}_account_$accountIndex" | ||||||
|     private val accountActiveKey get() = "${idPrefix}_active" |     private val accountActiveKey get() = "${idPrefix}_active" | ||||||
|  |  | ||||||
|  | @ -10,6 +10,9 @@ interface OAuth2API { | ||||||
|     val name: String |     val name: String | ||||||
|     val redirectUrl: String |     val redirectUrl: String | ||||||
| 
 | 
 | ||||||
|  |     // don't change this as all keys depend on it | ||||||
|  |     val idPrefix : String | ||||||
|  | 
 | ||||||
|     fun handleRedirect(context: Context, url: String) |     fun handleRedirect(context: Context, url: String) | ||||||
|     fun authenticate(context: Context) |     fun authenticate(context: Context) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,11 +5,10 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||||
| 
 | 
 | ||||||
| //TODO dropbox sync | //TODO dropbox sync | ||||||
| class Dropbox : OAuth2API { | class Dropbox : OAuth2API { | ||||||
|  |     override val idPrefix = "dropbox" | ||||||
|     override val name = "Dropbox" |     override val name = "Dropbox" | ||||||
|     override val key: String |     override val key = "zlqsamadlwydvb2" | ||||||
|         get() = "zlqsamadlwydvb2" |     override val redirectUrl = "dropboxlogin" | ||||||
|     override val redirectUrl: String |  | ||||||
|         get() = "dropboxlogin" |  | ||||||
| 
 | 
 | ||||||
|     override fun authenticate(context: Context) { |     override fun authenticate(context: Context) { | ||||||
|         TODO("Not yet implemented") |         TODO("Not yet implemented") | ||||||
|  |  | ||||||
|  | @ -48,12 +48,11 @@ import com.lagradost.cloudstream3.utils.HOMEPAGE_API | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact | import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons | import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes | import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.setImage | import com.lagradost.cloudstream3.utils.UIHelper.setImage | ||||||
| import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager | import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager | ||||||
| import kotlinx.android.synthetic.main.activity_main.* |  | ||||||
| import kotlinx.android.synthetic.main.fragment_home.* | import kotlinx.android.synthetic.main.fragment_home.* | ||||||
| import java.util.* | import java.util.* | ||||||
| 
 | 
 | ||||||
|  | @ -124,15 +123,8 @@ class HomeFragment : Fragment() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun fixGrid() { |     private fun fixGrid() { | ||||||
|         val compactView = activity?.getGridIsCompact() ?: false |         activity?.getSpanCount()?.let { | ||||||
|         val spanCountLandscape = if (compactView) 2 else 6 |             currentSpan = it | ||||||
|         val spanCountPortrait = if (compactView) 1 else 3 |  | ||||||
|         val orientation = resources.configuration.orientation |  | ||||||
| 
 |  | ||||||
|         currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { |  | ||||||
|             spanCountLandscape |  | ||||||
|         } else { |  | ||||||
|             spanCountPortrait |  | ||||||
|         } |         } | ||||||
|         configEvent.invoke(currentSpan) |         configEvent.invoke(currentSpan) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -19,11 +19,8 @@ import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.mvvm.observe | import com.lagradost.cloudstream3.mvvm.observe | ||||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | ||||||
| import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | ||||||
| import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD | import com.lagradost.cloudstream3.ui.search.* | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchAdapter |  | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse | import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchHelper |  | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchViewModel |  | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper | import com.lagradost.cloudstream3.utils.UIHelper | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | import com.lagradost.cloudstream3.utils.UIHelper.navigate | ||||||
|  | @ -34,12 +31,22 @@ import java.util.concurrent.locks.ReentrantLock | ||||||
| 
 | 
 | ||||||
| class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
|     companion object { |     companion object { | ||||||
|         fun push(activity: Activity?, mainApi: Boolean = true, autoSearch: String? = null) { |         fun pushSearch(activity: Activity?, autoSearch: String? = null) { | ||||||
|             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { |             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { | ||||||
|                 putBoolean("mainapi", mainApi) |                 putBoolean("mainapi", true) | ||||||
|                 putString("autosearch", autoSearch) |                 putString("autosearch", autoSearch) | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         fun pushSync(activity: Activity?, autoSearch: String? = null, callback : (SearchClickCallback) -> Unit) { | ||||||
|  |             clickCallback = callback | ||||||
|  |             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { | ||||||
|  |                 putBoolean("mainapi", false) | ||||||
|  |                 putString("autosearch", autoSearch) | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var clickCallback : ((SearchClickCallback) -> Unit)? = null | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private val searchViewModel: SearchViewModel by activityViewModels() |     private val searchViewModel: SearchViewModel by activityViewModels() | ||||||
|  | @ -56,6 +63,11 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
|         return inflater.inflate(R.layout.quick_search, container, false) |         return inflater.inflate(R.layout.quick_search, container, false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         super.onDestroy() | ||||||
|  |         clickCallback = null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|         context?.fixPaddingStatusbar(quick_search_root) |         context?.fixPaddingStatusbar(quick_search_root) | ||||||
|  | @ -96,7 +108,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
| 
 | 
 | ||||||
|                         SearchHelper.handleSearchClickCallback(activity, callback) |                         SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|                     } else { |                     } else { | ||||||
|                         //TODO MAL RESPONSE |                         clickCallback?.invoke(callback) | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 else -> SearchHelper.handleSearchClickCallback(activity, callback) |                 else -> SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|  | @ -135,7 +147,7 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
|                 is Resource.Success -> { |                 is Resource.Success -> { | ||||||
|                     it.value.let { data -> |                     it.value.let { data -> | ||||||
|                         if (data.isNotEmpty()) { |                         if (data.isNotEmpty()) { | ||||||
|                             (cardSpace?.adapter as SearchAdapter?)?.apply { |                             (search_autofit_results?.adapter as SearchAdapter?)?.apply { | ||||||
|                                 cardList = data.toList() |                                 cardList = data.toList() | ||||||
|                                 notifyDataSetChanged() |                                 notifyDataSetChanged() | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import android.content.Context.CLIPBOARD_SERVICE | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
| import android.content.Intent.* | import android.content.Intent.* | ||||||
| import android.content.res.ColorStateList | import android.content.res.ColorStateList | ||||||
|  | import android.content.res.Configuration | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.view.LayoutInflater | import android.view.LayoutInflater | ||||||
|  | @ -15,7 +16,9 @@ import android.view.View | ||||||
| import android.view.View.GONE | import android.view.View.GONE | ||||||
| import android.view.View.VISIBLE | import android.view.View.VISIBLE | ||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
| import android.widget.Toast | import android.widget.Toast | ||||||
|  | import androidx.annotation.StringRes | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.core.content.FileProvider | import androidx.core.content.FileProvider | ||||||
| import androidx.core.view.isGone | import androidx.core.view.isGone | ||||||
|  | @ -39,6 +42,8 @@ import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | ||||||
| import com.lagradost.cloudstream3.mvvm.observe | import com.lagradost.cloudstream3.mvvm.observe | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.ui.WatchType | import com.lagradost.cloudstream3.ui.WatchType | ||||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD | import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD | ||||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | ||||||
|  | @ -47,6 +52,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton | ||||||
| import com.lagradost.cloudstream3.ui.player.PlayerData | import com.lagradost.cloudstream3.ui.player.PlayerData | ||||||
| import com.lagradost.cloudstream3.ui.player.PlayerFragment | import com.lagradost.cloudstream3.ui.player.PlayerFragment | ||||||
| import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment | import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment | ||||||
|  | import com.lagradost.cloudstream3.ui.search.SearchAdapter | ||||||
|  | import com.lagradost.cloudstream3.ui.search.SearchHelper | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | ||||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 | import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 | ||||||
| import com.lagradost.cloudstream3.utils.* | import com.lagradost.cloudstream3.utils.* | ||||||
|  | @ -57,12 +64,15 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast | ||||||
| import com.lagradost.cloudstream3.utils.Coroutines.main | import com.lagradost.cloudstream3.utils.Coroutines.main | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.getFolderName | import com.lagradost.cloudstream3.utils.DataStore.getFolderName | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||||
|  | import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync | ||||||
|  | import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync | ||||||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos | import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos | ||||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.checkWrite | import com.lagradost.cloudstream3.utils.UIHelper.checkWrite | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
|  | import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | import com.lagradost.cloudstream3.utils.UIHelper.navigate | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | ||||||
|  | @ -256,7 +266,65 @@ class ResultFragment : Fragment() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var startAction: Int? = null |     var startAction: Int? = null | ||||||
|     var startValue: Int? = null |     private var startValue: Int? = null | ||||||
|  | 
 | ||||||
|  |     private fun updateSync(id: Int) { | ||||||
|  |         val syncList = context?.getSync(id, SyncApis.map { it.idPrefix }) ?: return | ||||||
|  |         val list = ArrayList<Pair<SyncAPI, String>>() | ||||||
|  |         for (i in 0 until SyncApis.count()) { | ||||||
|  |             val res = syncList[i] ?: continue | ||||||
|  |             list.add(Pair(SyncApis[i], res)) | ||||||
|  |         } | ||||||
|  |         viewModel.updateSync(context, list) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { | ||||||
|  |         if (arg == null) { | ||||||
|  |             textView?.isVisible = false | ||||||
|  |         } else { | ||||||
|  |             val text = context?.getString(format)?.format(arg) | ||||||
|  |             if (text == null) { | ||||||
|  |                 textView?.isVisible = false | ||||||
|  |             } else { | ||||||
|  |                 textView?.isVisible = true | ||||||
|  |                 textView?.text = text | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setDuration(duration: Int?) { | ||||||
|  |         setFormatText(result_meta_duration, R.string.duration_format, duration) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setYear(year: Int?) { | ||||||
|  |         setFormatText(result_meta_year, R.string.year_format, year) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setRating(rating: Int?) { | ||||||
|  |         setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun setRecommendations(rec: List<SearchResponse>?) { | ||||||
|  |         return | ||||||
|  |         result_recommendations?.isGone = rec.isNullOrEmpty() | ||||||
|  |         rec?.let { list -> | ||||||
|  |             (result_recommendations?.adapter as SearchAdapter?)?.apply { | ||||||
|  |                 cardList = list | ||||||
|  |                 notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun fixGrid() { | ||||||
|  |         activity?.getSpanCount()?.let { count -> | ||||||
|  |             result_recommendations?.spanCount = count | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun onConfigurationChanged(newConfig: Configuration) { | ||||||
|  |         super.onConfigurationChanged(newConfig) | ||||||
|  |         fixGrid() | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private fun lateFixDownloadButton(show: Boolean) { |     private fun lateFixDownloadButton(show: Boolean) { | ||||||
|         if (!show || currentType?.isMovieType() == false) { |         if (!show || currentType?.isMovieType() == false) { | ||||||
|  | @ -273,6 +341,7 @@ class ResultFragment : Fragment() { | ||||||
|     @SuppressLint("SetTextI18n") |     @SuppressLint("SetTextI18n") | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         fixGrid() | ||||||
| 
 | 
 | ||||||
|         val restart = arguments?.getBoolean("restart") ?: false |         val restart = arguments?.getBoolean("restart") ?: false | ||||||
|         if (restart) { |         if (restart) { | ||||||
|  | @ -949,6 +1018,20 @@ class ResultFragment : Fragment() { | ||||||
|             currentId = it |             currentId = it | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         observe(viewModel.sync) { sync -> | ||||||
|  |             for (s in sync) { | ||||||
|  |                 when (s) { | ||||||
|  |                     is Resource.Success -> { | ||||||
|  |                         val d = s.value ?: continue | ||||||
|  |                         setDuration(d.duration) | ||||||
|  |                         setRating(d.publicScore) | ||||||
|  |                     } | ||||||
|  |                     else -> { | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         observe(viewModel.resultResponse) { data -> |         observe(viewModel.resultResponse) { data -> | ||||||
|             when (data) { |             when (data) { | ||||||
|                 is Resource.Success -> { |                 is Resource.Success -> { | ||||||
|  | @ -965,7 +1048,7 @@ class ResultFragment : Fragment() { | ||||||
|                             VPNStatus.Torrent -> getString(R.string.vpn_torrent) |                             VPNStatus.Torrent -> getString(R.string.vpn_torrent) | ||||||
|                             else -> "" |                             else -> "" | ||||||
|                         } |                         } | ||||||
|                         result_vpn?.visibility = if (api.vpnStatus == VPNStatus.None) GONE else VISIBLE |                         result_vpn?.isGone = api.vpnStatus == VPNStatus.None | ||||||
| 
 | 
 | ||||||
|                         result_info?.text = when (api.providerType) { |                         result_info?.text = when (api.providerType) { | ||||||
|                             ProviderType.MetaProvider -> getString(R.string.provider_info_meta) |                             ProviderType.MetaProvider -> getString(R.string.provider_info_meta) | ||||||
|  | @ -973,8 +1056,6 @@ class ResultFragment : Fragment() { | ||||||
|                         } |                         } | ||||||
|                         result_info?.isVisible = api.providerType == ProviderType.MetaProvider |                         result_info?.isVisible = api.providerType == ProviderType.MetaProvider | ||||||
| 
 | 
 | ||||||
|                         //result_bookmark_button.text = getString(R.string.type_watching) |  | ||||||
| 
 |  | ||||||
|                         currentHeaderName = d.name |                         currentHeaderName = d.name | ||||||
|                         currentType = d.type |                         currentType = d.type | ||||||
| 
 | 
 | ||||||
|  | @ -992,7 +1073,7 @@ class ResultFragment : Fragment() { | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         result_search?.setOnClickListener { |                         result_search?.setOnClickListener { | ||||||
|                             QuickSearchFragment.push(activity, true, d.name) |                             QuickSearchFragment.pushSearch(activity, d.name) | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         result_share?.setOnClickListener { |                         result_share?.setOnClickListener { | ||||||
|  | @ -1003,6 +1084,20 @@ class ResultFragment : Fragment() { | ||||||
|                             startActivity(createChooser(i, d.name)) |                             startActivity(createChooser(i, d.name)) | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|  |                         updateSync(d.getId()) | ||||||
|  |                         result_add_sync?.setOnClickListener { | ||||||
|  |                             QuickSearchFragment.pushSync(activity, d.name) { click -> | ||||||
|  |                                 context?.addSync(d.getId(), click.card.apiName, click.card.url)?.let { | ||||||
|  |                                     showToast( | ||||||
|  |                                         activity, | ||||||
|  |                                         context?.getString(R.string.added_sync_format)?.format(click.card.name), | ||||||
|  |                                         Toast.LENGTH_SHORT | ||||||
|  |                                     ) | ||||||
|  |                                 } | ||||||
|  |                                 updateSync(d.getId()) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|                         val metadataInfoArray = ArrayList<Pair<Int, String>>() |                         val metadataInfoArray = ArrayList<Pair<Int, String>>() | ||||||
|                         if (d is AnimeLoadResponse) { |                         if (d is AnimeLoadResponse) { | ||||||
|                             val status = when (d.showStatus) { |                             val status = when (d.showStatus) { | ||||||
|  | @ -1015,23 +1110,10 @@ class ResultFragment : Fragment() { | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         result_meta_year?.isGone = d.year == null |                         setDuration(d.duration) | ||||||
|                         result_meta_year?.text = d.year?.toString() ?: "" |                         setYear(d.year) | ||||||
|                         if (d.rating == null) { |                         setRating(d.rating) | ||||||
|                             result_meta_rating?.isVisible = false |                         setRecommendations(d.recommendations) | ||||||
|                         } else { |  | ||||||
|                             result_meta_rating?.isVisible = true |  | ||||||
|                             result_meta_rating?.text = "%.1f/10.0".format(d.rating!!.toFloat() / 10f).replace(",", ".") |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         val duration = d.duration |  | ||||||
|                         if (duration.isNullOrEmpty()) { |  | ||||||
|                             result_meta_duration?.isVisible = false |  | ||||||
|                         } else { |  | ||||||
|                             result_meta_duration?.isVisible = true |  | ||||||
|                             result_meta_duration?.text = |  | ||||||
|                                 if (duration.endsWith("min") || duration.endsWith("h")) duration else "${duration}min" |  | ||||||
|                         } |  | ||||||
| 
 | 
 | ||||||
|                         result_meta_site?.text = d.apiName |                         result_meta_site?.text = d.apiName | ||||||
| 
 | 
 | ||||||
|  | @ -1188,6 +1270,17 @@ class ResultFragment : Fragment() { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         val recAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let { | ||||||
|  |             SearchAdapter( | ||||||
|  |                 ArrayList(), | ||||||
|  |                 result_recommendations, | ||||||
|  |             ) { callback -> | ||||||
|  |                 SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         result_recommendations.adapter = recAdapter | ||||||
|  | 
 | ||||||
|         context?.let { ctx -> |         context?.let { ctx -> | ||||||
|             result_bookmark_button?.isVisible = ctx.isTvSettings() |             result_bookmark_button?.isVisible = ctx.isTvSettings() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull | ||||||
| import com.lagradost.cloudstream3.APIHolder.getId | import com.lagradost.cloudstream3.APIHolder.getId | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||||
|  | import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository | import com.lagradost.cloudstream3.ui.APIRepository | ||||||
| import com.lagradost.cloudstream3.ui.WatchType | import com.lagradost.cloudstream3.ui.WatchType | ||||||
| import com.lagradost.cloudstream3.utils.* | import com.lagradost.cloudstream3.utils.* | ||||||
|  | @ -85,6 +86,9 @@ class ResultViewModel : ViewModel() { | ||||||
|     private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() |     private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() | ||||||
|     val watchStatus: LiveData<WatchType> get() = _watchStatus |     val watchStatus: LiveData<WatchType> get() = _watchStatus | ||||||
| 
 | 
 | ||||||
|  |     private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData() | ||||||
|  |     val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync | ||||||
|  | 
 | ||||||
|     fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch { |     fun updateWatchStatus(context: Context, status: WatchType) = viewModelScope.launch { | ||||||
|         val currentId = id.value ?: return@launch |         val currentId = id.value ?: return@launch | ||||||
|         _watchStatus.postValue(status) |         _watchStatus.postValue(status) | ||||||
|  | @ -198,6 +202,17 @@ class ResultViewModel : ViewModel() { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch { | ||||||
|  |         if(context == null) return@launch | ||||||
|  | 
 | ||||||
|  |         val list = ArrayList<Resource<SyncAPI.SyncResult?>>() | ||||||
|  |         for (s in sync) { | ||||||
|  |             val result = safeApiCall { s.first.getResult(context, s.second) } | ||||||
|  |             list.add(result) | ||||||
|  |             _sync.postValue(list) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) { |     private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) { | ||||||
|         _episodes.postValue(list) |         _episodes.postValue(list) | ||||||
|         val set = HashMap<Int, Int>() |         val set = HashMap<Int, Int>() | ||||||
|  | @ -363,7 +378,7 @@ class ResultViewModel : ViewModel() { | ||||||
|                                     (mainId + index + 1).hashCode(), |                                     (mainId + index + 1).hashCode(), | ||||||
|                                     index, |                                     index, | ||||||
|                                     i.rating, |                                     i.rating, | ||||||
|                                     i.descript, |                                     i.description, | ||||||
|                                     null, |                                     null, | ||||||
|                                 ) |                                 ) | ||||||
|                             ) |                             ) | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import android.view.WindowManager | ||||||
| import android.widget.* | import android.widget.* | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.appcompat.widget.SearchView | import androidx.appcompat.widget.SearchView | ||||||
|  | import androidx.core.view.isVisible | ||||||
| import androidx.fragment.app.Fragment | import androidx.fragment.app.Fragment | ||||||
| import androidx.fragment.app.activityViewModels | import androidx.fragment.app.activityViewModels | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
|  | @ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.APIRepository | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive | import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive | import com.lagradost.cloudstream3.ui.APIRepository.Companion.typesActive | ||||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment | import com.lagradost.cloudstream3.ui.home.HomeFragment | ||||||
|  | import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan | ||||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | ||||||
| import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | ||||||
|  | @ -35,7 +37,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||||
| import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE | import com.lagradost.cloudstream3.utils.SEARCH_PROVIDER_TOGGLE | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact | import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||||
| import kotlinx.android.synthetic.main.fragment_search.* | import kotlinx.android.synthetic.main.fragment_search.* | ||||||
| import java.util.concurrent.locks.ReentrantLock | import java.util.concurrent.locks.ReentrantLock | ||||||
|  | @ -68,18 +70,11 @@ class SearchFragment : Fragment() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun fixGrid() { |     private fun fixGrid() { | ||||||
|         val compactView = activity?.getGridIsCompact() ?: false |         activity?.getSpanCount()?.let { | ||||||
|         val spanCountLandscape = if (compactView) 2 else 6 |             currentSpan = it | ||||||
|         val spanCountPortrait = if (compactView) 1 else 3 |  | ||||||
|         val orientation = resources.configuration.orientation |  | ||||||
| 
 |  | ||||||
|         val currentSpan = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { |  | ||||||
|             spanCountLandscape |  | ||||||
|         } else { |  | ||||||
|             spanCountPortrait |  | ||||||
|         } |         } | ||||||
|         cardSpace.spanCount = currentSpan |         search_autofit_results.spanCount = currentSpan | ||||||
|         HomeFragment.currentSpan = currentSpan |         currentSpan = currentSpan | ||||||
|         HomeFragment.configEvent.invoke(currentSpan) |         HomeFragment.configEvent.invoke(currentSpan) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -102,13 +97,13 @@ class SearchFragment : Fragment() { | ||||||
|         val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let { |         val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let { | ||||||
|             SearchAdapter( |             SearchAdapter( | ||||||
|                 ArrayList(), |                 ArrayList(), | ||||||
|                 cardSpace, |                 search_autofit_results, | ||||||
|             ) { callback -> |             ) { callback -> | ||||||
|                 SearchHelper.handleSearchClickCallback(activity, callback) |                 SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         cardSpace.adapter = adapter |         search_autofit_results.adapter = adapter | ||||||
|         search_loading_bar.alpha = 0f |         search_loading_bar.alpha = 0f | ||||||
| 
 | 
 | ||||||
|         val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) |         val searchExitIcon = main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) | ||||||
|  | @ -325,7 +320,7 @@ class SearchFragment : Fragment() { | ||||||
|                 is Resource.Success -> { |                 is Resource.Success -> { | ||||||
|                     it.value.let { data -> |                     it.value.let { data -> | ||||||
|                         if (data.isNotEmpty()) { |                         if (data.isNotEmpty()) { | ||||||
|                             (cardSpace?.adapter as SearchAdapter?)?.apply { |                             (search_autofit_results?.adapter as SearchAdapter?)?.apply { | ||||||
|                                 cardList = data.toList() |                                 cardList = data.toList() | ||||||
|                                 notifyDataSetChanged() |                                 notifyDataSetChanged() | ||||||
|                             } |                             } | ||||||
|  | @ -393,8 +388,8 @@ class SearchFragment : Fragment() { | ||||||
|         val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) |         val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) | ||||||
|         val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true) |         val isAdvancedSearch = settingsManager.getBoolean("advanced_search", true) | ||||||
| 
 | 
 | ||||||
|         search_master_recycler.visibility = if (isAdvancedSearch) View.VISIBLE else View.GONE |         search_master_recycler.isVisible = isAdvancedSearch | ||||||
|         cardSpace.visibility = if (!isAdvancedSearch) View.VISIBLE else View.GONE |         search_autofit_results.isVisible = !isAdvancedSearch | ||||||
| 
 | 
 | ||||||
|         // SubtitlesFragment.push(activity) |         // SubtitlesFragment.push(activity) | ||||||
|         //searchViewModel.search("iron man") |         //searchViewModel.search("iron man") | ||||||
|  |  | ||||||
|  | @ -118,7 +118,7 @@ object DataStoreHelper { | ||||||
| 
 | 
 | ||||||
|     fun Context.setViewPos(id: Int?, pos: Long, dur: Long) { |     fun Context.setViewPos(id: Int?, pos: Long, dur: Long) { | ||||||
|         if (id == null) return |         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)) |         setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -146,6 +146,16 @@ object DataStoreHelper { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun Context.setResultSeason(id: Int, value: Int?) { |     fun Context.setResultSeason(id: Int, value: Int?) { | ||||||
|         return setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) |         setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun Context.addSync(id: Int, idPrefix: String, url: String) { | ||||||
|  |         setKey("${idPrefix}_sync", id.toString(), url) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun Context.getSync(id : Int, idPrefixes : List<String>) : List<String?> { | ||||||
|  |         return idPrefixes.map { idPrefix -> | ||||||
|  |             getKey("${idPrefix}_sync", id.toString()) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -7,6 +7,7 @@ import android.app.AppOpsManager | ||||||
| import android.app.Dialog | import android.app.Dialog | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.pm.PackageManager | import android.content.pm.PackageManager | ||||||
|  | import android.content.res.Configuration | ||||||
| import android.content.res.Resources | import android.content.res.Resources | ||||||
| import android.graphics.Color | import android.graphics.Color | ||||||
| import android.os.Build | import android.os.Build | ||||||
|  | @ -68,6 +69,19 @@ object UIHelper { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun Activity?.getSpanCount() : Int? { | ||||||
|  |         val compactView = this?.getGridIsCompact() ?: return null | ||||||
|  |         val spanCountLandscape = if (compactView) 2 else 6 | ||||||
|  |         val spanCountPortrait = if (compactView) 1 else 3 | ||||||
|  |         val orientation = this.resources?.configuration?.orientation ?: return null | ||||||
|  | 
 | ||||||
|  |         return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { | ||||||
|  |             spanCountLandscape | ||||||
|  |         } else { | ||||||
|  |             spanCountPortrait | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fun Fragment.hideKeyboard() { |     fun Fragment.hideKeyboard() { | ||||||
|         activity?.window?.decorView?.clearFocus() |         activity?.window?.decorView?.clearFocus() | ||||||
|         view?.let { |         view?.let { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ object VideoDownloadHelper { | ||||||
|         override val id: Int, |         override val id: Int, | ||||||
|         val parentId: Int, |         val parentId: Int, | ||||||
|         val rating: Int?, |         val rating: Int?, | ||||||
|         val descript: String?, |         val description: String?, | ||||||
|         val cacheTime: Long, |         val cacheTime: Long, | ||||||
|     ) : EasyDownloadButton.IMinimumData |     ) : EasyDownloadButton.IMinimumData | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_baseline_add_24.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_baseline_add_24.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | <vector android:height="24dp" android:tint="#FFFFFF" | ||||||
|  |     android:viewportHeight="24" android:viewportWidth="24" | ||||||
|  |     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> | ||||||
|  | </vector> | ||||||
|  | @ -187,9 +187,30 @@ | ||||||
|                                 app:mediaRouteButtonTint="?attr/textColor" |                                 app:mediaRouteButtonTint="?attr/textColor" | ||||||
|                         /> |                         /> | ||||||
|                         <ImageView |                         <ImageView | ||||||
|  |                                 android:visibility="gone" | ||||||
|                                 android:nextFocusUp="@id/result_back" |                                 android:nextFocusUp="@id/result_back" | ||||||
|                                 android:nextFocusDown="@id/result_descript" |                                 android:nextFocusDown="@id/result_descript" | ||||||
|                                 android:nextFocusLeft="@id/result_back" |                                 android:nextFocusLeft="@id/result_back" | ||||||
|  |                                 android:nextFocusRight="@id/result_share" | ||||||
|  | 
 | ||||||
|  |                                 android:id="@+id/result_add_sync" | ||||||
|  |                                 android:layout_width="30dp" | ||||||
|  |                                 android:layout_height="30dp" | ||||||
|  |                                 android:layout_marginRight="10dp" | ||||||
|  |                                 android:elevation="10dp" | ||||||
|  | 
 | ||||||
|  |                                 android:tint="?attr/textColor" | ||||||
|  | 
 | ||||||
|  |                                 android:background="?android:attr/selectableItemBackgroundBorderless" | ||||||
|  |                                 android:src="@drawable/ic_baseline_add_24" | ||||||
|  |                                 android:layout_gravity="end|center_vertical" | ||||||
|  |                                 android:contentDescription="@string/add_sync"> | ||||||
|  |                         </ImageView> | ||||||
|  | 
 | ||||||
|  |                         <ImageView | ||||||
|  |                                 android:nextFocusUp="@id/result_back" | ||||||
|  |                                 android:nextFocusDown="@id/result_descript" | ||||||
|  |                                 android:nextFocusLeft="@id/result_add_sync" | ||||||
|                                 android:nextFocusRight="@id/result_openinbrower" |                                 android:nextFocusRight="@id/result_openinbrower" | ||||||
| 
 | 
 | ||||||
|                                 android:id="@+id/result_share" |                                 android:id="@+id/result_share" | ||||||
|  | @ -522,89 +543,115 @@ | ||||||
|                                 android:layout_height="match_parent"> |                                 android:layout_height="match_parent"> | ||||||
|                         </TextView> |                         </TextView> | ||||||
|                     </LinearLayout> |                     </LinearLayout> | ||||||
| 
 |                     <com.google.android.material.tabs.TabLayout | ||||||
|                     <LinearLayout |                             android:visibility="gone" | ||||||
|                             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_width="match_parent" | ||||||
|                             android:layout_height="wrap_content" |                             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> | ||||||
|             </LinearLayout> |             </LinearLayout> | ||||||
|         </androidx.core.widget.NestedScrollView> |         </androidx.core.widget.NestedScrollView> | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ | ||||||
|                     android:nextFocusUp="@id/nav_rail_view" |                     android:nextFocusUp="@id/nav_rail_view" | ||||||
|                     android:nextFocusRight="@id/search_filter" |                     android:nextFocusRight="@id/search_filter" | ||||||
|                     android:nextFocusLeft="@id/nav_rail_view" |                     android:nextFocusLeft="@id/nav_rail_view" | ||||||
|                     android:nextFocusDown="@id/cardSpace" |                     android:nextFocusDown="@id/search_autofit_results" | ||||||
| 
 | 
 | ||||||
|                     android:imeOptions="actionSearch" |                     android:imeOptions="actionSearch" | ||||||
|                     android:inputType="text" |                     android:inputType="text" | ||||||
|  | @ -65,7 +65,7 @@ | ||||||
|                 android:nextFocusUp="@id/nav_rail_view" |                 android:nextFocusUp="@id/nav_rail_view" | ||||||
|                 android:nextFocusRight="@id/main_search" |                 android:nextFocusRight="@id/main_search" | ||||||
|                 android:nextFocusLeft="@id/main_search" |                 android:nextFocusLeft="@id/main_search" | ||||||
|                 android:nextFocusDown="@id/cardSpace" |                 android:nextFocusDown="@id/search_autofit_results" | ||||||
| 
 | 
 | ||||||
|                 android:id="@+id/search_filter" |                 android:id="@+id/search_filter" | ||||||
|                 android:background="?selectableItemBackgroundBorderless" |                 android:background="?selectableItemBackgroundBorderless" | ||||||
|  | @ -91,7 +91,7 @@ | ||||||
|             android:paddingTop="5dp" |             android:paddingTop="5dp" | ||||||
|             app:spanCount="3" |             app:spanCount="3" | ||||||
|             android:paddingEnd="8dp" |             android:paddingEnd="8dp" | ||||||
|             android:id="@+id/cardSpace" |             android:id="@+id/search_autofit_results" | ||||||
|             tools:listitem="@layout/search_result_grid" |             tools:listitem="@layout/search_result_grid" | ||||||
|             android:orientation="vertical" |             android:orientation="vertical" | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
|             <androidx.appcompat.widget.SearchView |             <androidx.appcompat.widget.SearchView | ||||||
|                     android:nextFocusRight="@id/search_filter" |                     android:nextFocusRight="@id/search_filter" | ||||||
|                     android:nextFocusLeft="@id/search_filter" |                     android:nextFocusLeft="@id/search_filter" | ||||||
|                     android:nextFocusDown="@id/cardSpace" |                     android:nextFocusDown="@id/search_autofit_results" | ||||||
| 
 | 
 | ||||||
|                     android:imeOptions="actionSearch" |                     android:imeOptions="actionSearch" | ||||||
|                     android:inputType="text" |                     android:inputType="text" | ||||||
|  |  | ||||||
|  | @ -41,7 +41,9 @@ | ||||||
|     <string name="rew_text_format" translatable="false" formatted="true">-%d</string> |     <string name="rew_text_format" translatable="false" formatted="true">-%d</string> | ||||||
|     <string name="ffw_text_regular_format" translatable="false" formatted="true">%d</string> |     <string name="ffw_text_regular_format" translatable="false" formatted="true">%d</string> | ||||||
|     <string name="rew_text_regular_format" translatable="false" formatted="true">%d</string> |     <string name="rew_text_regular_format" translatable="false" formatted="true">%d</string> | ||||||
|     <string name="app_dub_sub_episode_text_format">%s Ep %d</string> |     <string name="rating_format" translatable="false" formatted="true">%.1f/10.0</string> | ||||||
|  |     <string name="year_format" translatable="false" formatted="true">%d</string> | ||||||
|  |     <string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string> | ||||||
| 
 | 
 | ||||||
|     <!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS --> |     <!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS --> | ||||||
|     <string name="result_poster_img_des">Poster</string> |     <string name="result_poster_img_des">Poster</string> | ||||||
|  | @ -60,6 +62,7 @@ | ||||||
|     <string name="rated_format" formatted="true">Rated: %.1f</string> |     <string name="rated_format" formatted="true">Rated: %.1f</string> | ||||||
|     <string name="new_update_format" formatted="true">New update found!\n%s -> %s</string> |     <string name="new_update_format" formatted="true">New update found!\n%s -> %s</string> | ||||||
|     <string name="filler_format" formatted="true">(Filler) %s</string> |     <string name="filler_format" formatted="true">(Filler) %s</string> | ||||||
|  |     <string name="duration_format" formatted="true">%d min</string> | ||||||
| 
 | 
 | ||||||
|     <string name="app_name">CloudStream</string> |     <string name="app_name">CloudStream</string> | ||||||
|     <string name="title_home">Home</string> |     <string name="title_home">Home</string> | ||||||
|  | @ -223,7 +226,7 @@ | ||||||
|     <string name="cancel" translatable="false">@string/sort_cancel</string> |     <string name="cancel" translatable="false">@string/sort_cancel</string> | ||||||
|     <string name="pause">Pause</string> |     <string name="pause">Pause</string> | ||||||
|     <string name="resume">Resume</string> |     <string name="resume">Resume</string> | ||||||
|     <string name="delete_message">This will permanently delete %s\nAre you sure?</string> |     <string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string> | ||||||
| 
 | 
 | ||||||
|     <string name="status_ongoing">Ongoing</string> |     <string name="status_ongoing">Ongoing</string> | ||||||
|     <string name="status_completed">Completed</string> |     <string name="status_completed">Completed</string> | ||||||
|  | @ -337,12 +340,14 @@ | ||||||
|     <string name="kitsu_account_settings" translatable="false">Kitsu</string> |     <string name="kitsu_account_settings" translatable="false">Kitsu</string> | ||||||
|     <string name="trakt_account_settings" translatable="false">Trakt</string> |     <string name="trakt_account_settings" translatable="false">Trakt</string> | ||||||
|     --> |     --> | ||||||
|     <string name="login_format">%s %s</string> |     <string name="login_format" formatted="true">%s %s</string> | ||||||
|     <string name="account">account</string> |     <string name="account">account</string> | ||||||
|     <string name="logout">Logout</string> |     <string name="logout">Logout</string> | ||||||
|     <string name="login">Login</string> |     <string name="login">Login</string> | ||||||
|     <string name="switch_account">Switch account</string> |     <string name="switch_account">Switch account</string> | ||||||
|     <string name="add_account">Add account</string> |     <string name="add_account">Add account</string> | ||||||
|  |     <string name="add_sync">Add tracking</string> | ||||||
|  |     <string name="added_sync_format" formatted="true">Added %s</string> | ||||||
|     <!-- ============ --> |     <!-- ============ --> | ||||||
|     <string name="none">None</string> |     <string name="none">None</string> | ||||||
|     <string name="normal">Normal</string> |     <string name="normal">Normal</string> | ||||||
|  | @ -361,4 +366,6 @@ | ||||||
| 	https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog | 	https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog | ||||||
|     --> |     --> | ||||||
|     <string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string> |     <string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string> | ||||||
|  | 
 | ||||||
|  |     <string name="tab_recommended">Recommended</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue