Compare commits
12 Commits
995efeda29
...
6bd544b335
Author | SHA1 | Date |
---|---|---|
Kai Kitching | 6bd544b335 | |
int3debug | e6b9d621f9 | |
KingLucius | 0019f85501 | |
IndusAryan | 0744189020 | |
Kai Kitching | 434cb87784 | |
Kai Kitching | 8dde852dfc | |
Kai Kitching | 45b6a8d60d | |
Kai Kitching | f726c1e888 | |
Kai Kitching | a542efe43b | |
Kai Kitching | a2bd2ea08d | |
Kai Kitching | 96fa3de474 | |
Kai Kitching | f2579a2a32 |
103
README.md
103
README.md
|
@ -1,11 +1,44 @@
|
|||
# CloudStream
|
||||
|
||||
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
|
||||
**⚠️ Warning: By default, this app doesn't provide any video sources; you have to install extensions to add functionality to the app.**
|
||||
|
||||
## Table of Contents:
|
||||
+ [About Us:](#about_us)
|
||||
+ [Discord Server:](#discord)
|
||||
+ [Installation Steps:](#install_rules)
|
||||
+ [Contributing:](#contributing)
|
||||
+ [Issues:](#issues)
|
||||
+ [Bugs Reports:](#bug_report)
|
||||
+ [Enhancement:](#enhancment)
|
||||
+ [Extension Development:](#extensions)
|
||||
+ [Languauge Support:](#languages)
|
||||
+ [Further Sources](#contact_and_sources)
|
||||
|
||||
|
||||
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
|
||||
<a id="about_us"></a>
|
||||
|
||||
### Features:
|
||||
## About us:
|
||||
|
||||
**CloudStream is a media center that prioritizes and emphasizes complete freedom and flexibility for users and developers.**
|
||||
|
||||
CloudStream is an extension-based multimedia player with tracking support. There are extensions to view videos from:
|
||||
|
||||
+ [Librevox (audio-books)](https://librivox.org/)
|
||||
+ [Youtube](https://www.youtube.com/)
|
||||
+ [Twitch](https://www.twitch.tv/)
|
||||
+ [iptv-org (A collection of publicly available IPTV (Internet Protocol television) channels from all over the world.)](https://github.com/iptv-org/iptv)
|
||||
+ [nginx](https://nginx.org/)
|
||||
+ And more...
|
||||
|
||||
|
||||
**Please don't create illegal extensions or use any that host any copyrighted media.** For more details about our stance on the DMCA and EUCD, you can read about it on our organization: [reCloudStream](https://github.com/recloudstream)
|
||||
|
||||
#### Important Copyright Note:
|
||||
|
||||
Our documentation is unmaintained and open to contributions; therefore, apps and sources, extensions in recommended sources, and recommended apps are not officially moderated or endorsed by CloudStream; if you or another copyright owner identify an extension that breaches your copyright, please let us know.
|
||||
|
||||
|
||||
#### Features:
|
||||
+ **AdFree**, No ads whatsoever
|
||||
+ No tracking/analytics
|
||||
+ Bookmarks
|
||||
|
@ -13,7 +46,71 @@
|
|||
+ Chromecast
|
||||
+ Extension system for personal customization
|
||||
|
||||
<a id="discord"></a>
|
||||
|
||||
### Discord Server:
|
||||
|
||||
We have a Discord server that hosts our passionate community; if you wish to talk with the project's maintainers, have questions about CloudStream, or just hang out, please join our Discord server through the link below.
|
||||
|
||||
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
|
||||
|
||||
<a id="install_rules"></a>
|
||||
|
||||
## Installation:
|
||||
|
||||
Our documentation provides the steps to Install Cloudstream. The steps are simple and extensively customizable for your streaming needs!
|
||||
|
||||
[Getting Started With CloudStream:](https://recloudstream.github.io/csdocs/)
|
||||
|
||||
<a id="contributing"></a>
|
||||
|
||||
## Contributing:
|
||||
We **happily** accept any contributions to our project. To find out where you can start contributing towards the project, please look [at our issues tab](/cloudstream/issues)
|
||||
|
||||
|
||||
|
||||
<a id="issues"></a>
|
||||
|
||||
### Issues:
|
||||
While we **actively** accept issues and pull requests; We do require you fill out an [template](https://github.com/recloudstream/cloudstream/issues/new/choose) for issues, these include the following:
|
||||
|
||||
<a id="bug_report"></a>
|
||||
|
||||
- [Bug Report Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=bug&projects=&template=application-bug.yml)
|
||||
- For bug reports, we want as much info as possible, including your downloaded version of CloudeStream, device and updated version (if possible, current API),
|
||||
expected behavior of the program, and the actual behavior that the program did, most importantly we require clear, reproducible steps of the bug. If your bug can't be reproduced, it is unlikely we'll work on your issue.
|
||||
|
||||
<a id="#enhancment">
|
||||
|
||||
- [Feature Request Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=enhancement&projects=&template=feature-request.yml)
|
||||
- Before adding a feature request, please check to see if a feature request already has been requested.
|
||||
|
||||
|
||||
### Extensions:
|
||||
|
||||
**Further details on creating extensions for CloudStream are found in our documentation. **
|
||||
|
||||
[Guide: For Extension Developers](https://recloudstream.github.io/csdocs/devs/gettingstarted/)
|
||||
|
||||
<a id="contact_and_sources"></a>
|
||||
|
||||
## Further Sources:
|
||||
|
||||
As well as providing clear install steps, our [website](https://dweb.link/ipns/cloudstream.on.fleek.co/) includes a wide variety of other tools, such as:
|
||||
- [Troubleshooting](https://recloudstream.github.io/csdocs/troubleshooting/)
|
||||
- [Further CloudStream Repositories](https://recloudstream.github.io/csdocs/repositories/)
|
||||
- Set-Up for other devices, such as:
|
||||
- [Android TV](https://recloudstream.github.io/csdocs/other-devices/tv/)
|
||||
- [Windows](https://recloudstream.github.io/csdocs/other-devices/windows/)
|
||||
- [Linux](https://recloudstream.github.io/csdocs/other-devices/linux/)
|
||||
- And more...
|
||||
|
||||
<a id="languages"> </a>
|
||||
|
||||
### Supported languages:
|
||||
|
||||
Even if you can't contribute to the code or documentation, we always look for those who can contribute to translation and language support. Your contribution is exceptionally appreciated; you can check our translation from the figure below.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
|
|
@ -0,0 +1,430 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import java.util.Locale
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class TraktProvider : MainAPI() {
|
||||
override var name = "Trakt"
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
||||
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
||||
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
||||
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
||||
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
|
||||
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
return newHomePageResponse(request.name, results)
|
||||
}
|
||||
|
||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||
|
||||
val media = this.media ?: this
|
||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||
val poster = media.images?.poster?.firstOrNull()
|
||||
|
||||
if (mediaType == TvType.Movie) {
|
||||
return newMovieSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.Movie,
|
||||
) {
|
||||
posterUrl = fixPath(poster)
|
||||
}
|
||||
} else {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.TvSeries,
|
||||
) {
|
||||
this.posterUrl = fixPath(poster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
|
||||
val data = parseJson<Data>(url)
|
||||
val mediaDetails = data.mediaDetails
|
||||
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
||||
|
||||
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
||||
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
||||
|
||||
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
||||
|
||||
val actors = parseJson<People>(resActor).cast?.map {
|
||||
ActorData(
|
||||
Actor(
|
||||
name = it.person?.name!!,
|
||||
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
||||
),
|
||||
roleString = it.character
|
||||
)
|
||||
}
|
||||
|
||||
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
||||
|
||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||
|
||||
val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
||||
val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
||||
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
||||
val isBollywood = mediaDetails?.country == "in"
|
||||
|
||||
if (data.type == TvType.Movie) {
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
type = data.type.toString(),
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
airedDate = mediaDetails?.released
|
||||
?: mediaDetails?.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
).toJson()
|
||||
|
||||
return newMovieLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
dataUrl = linkData.toJson(),
|
||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.apiName = "Trakt"
|
||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||
val seasonsNames = mutableListOf<SeasonData>()
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
seasonsNames.add(
|
||||
SeasonData(
|
||||
season.number!!,
|
||||
season.title
|
||||
)
|
||||
)
|
||||
|
||||
season.episodes?.map { episode ->
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
type = data.type.toString(),
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
airedYear = mediaDetails?.year,
|
||||
lastSeason = seasons.size,
|
||||
epsTitle = episode.title,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
date = episode.firstAired,
|
||||
airedDate = episode.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
isCartoon = isCartoon
|
||||
).toJson()
|
||||
|
||||
episodes.add(
|
||||
Episode(
|
||||
data = linkData.toJson(),
|
||||
name = episode.title,
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
|
||||
rating = episode.rating?.times(10)?.roundToInt(),
|
||||
description = episode.overview,
|
||||
).apply {
|
||||
this.addDate(episode.firstAired)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
||||
episodes = episodes
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.apiName = "Trakt"
|
||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||
this.episodes = episodes
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.showStatus = getStatus(mediaDetails.status)
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.seasonNames = seasonsNames
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getApi(url: String) : String {
|
||||
return app.get(
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"trakt-api-version" to "2",
|
||||
"trakt-api-key" to traktClientId,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private fun isUpcoming(dateString: String?): Boolean {
|
||||
return try {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||
APIHolder.unixTimeMS < dateTime
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(t: String?): ShowStatus {
|
||||
return when (t) {
|
||||
"returning series" -> ShowStatus.Ongoing
|
||||
"continuing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixPath(url: String?): String? {
|
||||
url ?: return null
|
||||
return "https://$url"
|
||||
}
|
||||
|
||||
private fun getWidthImageUrl(path: String?, width: String) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
val fileName = Uri.parse(path).lastPathSegment ?: return null
|
||||
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
||||
}
|
||||
|
||||
private fun getOriginalWidthImageUrl(path: String?) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
return getWidthImageUrl(path, "original")
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val type: TvType? = null,
|
||||
val mediaDetails: MediaDetails? = null,
|
||||
)
|
||||
|
||||
data class MediaDetails(
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("released") val released: String? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||
)
|
||||
|
||||
data class Airs(
|
||||
@JsonProperty("day") val day: String? = null,
|
||||
@JsonProperty("time") val time: String? = null,
|
||||
@JsonProperty("timezone") val timezone: String? = null,
|
||||
)
|
||||
|
||||
data class Ids(
|
||||
@JsonProperty("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||
)
|
||||
|
||||
data class Images(
|
||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") val clearart: List<String>? = null,
|
||||
@JsonProperty("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||
)
|
||||
|
||||
data class People(
|
||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||
)
|
||||
|
||||
data class Cast(
|
||||
@JsonProperty("character") val character: String? = null,
|
||||
@JsonProperty("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") val person: Person? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Person(
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Seasons(
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class TraktEpisode(
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class LinkData(
|
||||
val id: Int? = null,
|
||||
val imdbId: String? = null,
|
||||
val tvdbId: Int? = null,
|
||||
val type: String? = null,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val aniId: String? = null,
|
||||
val animeId: String? = null,
|
||||
val title: String? = null,
|
||||
val year: Int? = null,
|
||||
val orgTitle: String? = null,
|
||||
val isAnime: Boolean = false,
|
||||
val airedYear: Int? = null,
|
||||
val lastSeason: Int? = null,
|
||||
val epsTitle: String? = null,
|
||||
val jpTitle: String? = null,
|
||||
val date: String? = null,
|
||||
val airedDate: String? = null,
|
||||
val isAsian: Boolean = false,
|
||||
val isBollywood: Boolean = false,
|
||||
val isCartoon: Boolean = false,
|
||||
)
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.media.metrics.PlaybackErrorEvent
|
||||
|
@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import androidx.media3.ui.*
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.github.rubensousa.previewseekbar.PreviewBar
|
||||
|
@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
is VideoEndedEvent -> {
|
||||
context?.let { ctx ->
|
||||
// Resets subtitle delay on ended video
|
||||
player.setSubtitleOffset(0)
|
||||
|
||||
// Only play next episode if autoplay is on (default)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
?.getBoolean(
|
||||
|
|
|
@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
Player.STATE_ENDED -> {
|
||||
// Resets subtitle delay on ended video
|
||||
setSubtitleOffset(0)
|
||||
|
||||
// Only play next episode if autoplay is on (default)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||
?.getBoolean(
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
|
@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
|
@ -133,7 +135,6 @@ class SettingsFragment : Fragment() {
|
|||
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.main_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -141,21 +142,44 @@ class SettingsFragment : Fragment() {
|
|||
activity?.navigate(id, Bundle())
|
||||
}
|
||||
|
||||
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
|
||||
/** used to debug leaks
|
||||
showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} :
|
||||
${VideoDownloadManager.downloadProgressEvent.size}") **/
|
||||
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
if (binding?.settingsProfilePic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
binding?.settingsProfileText?.text = login.name
|
||||
binding?.settingsProfile?.isVisible = true
|
||||
break
|
||||
fun hasProfilePictureFromAccountManagers(accountManagers: List<AccountManager>): Boolean {
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
|
||||
if (binding?.settingsProfilePic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
binding?.settingsProfileText?.text = login.name
|
||||
return true // sync profile exists
|
||||
}
|
||||
}
|
||||
return false // not syncing
|
||||
}
|
||||
|
||||
// display local account information if not syncing
|
||||
if (!hasProfilePictureFromAccountManagers(accountManagers)) {
|
||||
val activity = activity ?: return
|
||||
val currentAccount = try {
|
||||
DataStoreHelper.accounts.firstOrNull {
|
||||
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||
} ?: activity.let { DataStoreHelper.getDefaultAccount(activity) }
|
||||
|
||||
} catch (t: IllegalStateException) {
|
||||
Log.e("AccountManager", "Activity not found", t)
|
||||
null
|
||||
}
|
||||
|
||||
binding?.settingsProfilePic?.setImage(currentAccount?.image)
|
||||
binding?.settingsProfileText?.text = currentAccount?.name
|
||||
}
|
||||
|
||||
binding?.apply {
|
||||
listOf(
|
||||
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@android:color/white">
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?attr/white" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
|
@ -24,7 +24,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
|
@ -36,7 +35,11 @@
|
|||
android:id="@+id/settings_profile_pic"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
android:scaleType="centerCrop"
|
||||
android:foreground="@drawable/rounded_outline"
|
||||
tools:src="@drawable/profile_bg_orange"
|
||||
android:contentDescription="@string/account"/>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
|
@ -50,7 +53,7 @@
|
|||
android:textColor="?attr/textColor"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Hello world" />
|
||||
tools:text="Quick Brown Fox" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
|
|
Loading…
Reference in New Issue