From e864d28b354b9c3342b0b999702d835ab8697204 Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:10:19 +0100 Subject: [PATCH] add trailers (not complete) --- app/build.gradle | 8 +- .../com/lagradost/cloudstream3/MainAPI.kt | 5 + .../metaproviders/TmdbProvider.kt | 261 ++++++++++++++++++ .../movieproviders/TrailersTwoProvider.kt | 174 ++++++++++++ .../lagradost/cloudstream3/utils/AppUtils.kt | 34 ++- 5 files changed, 474 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersTwoProvider.kt diff --git a/app/build.gradle b/app/build.gradle index 933f9db7..e1b20711 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,8 +142,8 @@ dependencies { //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0' // Downloading - implementation "androidx.work:work-runtime:2.7.0" - implementation "androidx.work:work-runtime-ktx:2.7.0" + implementation "androidx.work:work-runtime:2.7.1" + implementation "androidx.work:work-runtime-ktx:2.7.1" // Networking implementation "com.squareup.okhttp3:okhttp:4.9.1" @@ -152,6 +152,10 @@ dependencies { // Util to skip the URI file fuckery 🙏 implementation "com.github.tachiyomiorg:unifile:17bec43" + // API because cba maintaining it myself + implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0" + + // debugImplementation because LeakCanary should only run in debug builds. // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 3be39506..0811e43e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.animeproviders.* +import com.lagradost.cloudstream3.metaproviders.TmdbProvider import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.utils.ExtractorLink import java.util.* @@ -52,6 +53,10 @@ object APIHolder { SflixProvider("https://sflix.to","Sflix"), SflixProvider("https://dopebox.to","Dopebox"), +// TmdbProvider(), + +// TrailersTwoProvider(), + ZoroProvider() ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt new file mode 100644 index 00000000..f044ede6 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -0,0 +1,261 @@ +package com.lagradost.cloudstream3.metaproviders + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.uwetrottmann.tmdb2.Tmdb +import com.uwetrottmann.tmdb2.entities.* +import java.util.* + +open class TmdbProvider : MainAPI() { + + open val useMetaLoadResponse = false + open val apiName = "TMDB" + + override val hasMainPage: Boolean + get() = true + + val tmdb = Tmdb("TMDB_KEY_HERE") + + private fun getImageUrl(link: String?): String? { + if (link == null) return null + return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link + } + + private fun getUrl(id: Int?, tvShow: Boolean): String { + return if (tvShow) "https://www.themoviedb.org/tv/${id ?: -1}" + else "https://www.themoviedb.org/movie/${id ?: -1}" + } + + /** + * episode and season starting from 1 + * they are null if movie + * */ + data class TmdbLink( + @JsonProperty("imdbID") val imdbID: String?, + @JsonProperty("tmdbID") val tmdbID: Int?, + @JsonProperty("episode") val episode: Int?, + @JsonProperty("season") val season: Int? + ) + + + private fun BaseTvShow.toSearchResponse(): TvSeriesSearchResponse { + return TvSeriesSearchResponse( + this.name ?: this.original_name, + getUrl(id, true), + apiName, + TvType.TvSeries, + getImageUrl(this.poster_path), + this.first_air_date?.let { + Calendar.getInstance().apply { + time = it + }.get(Calendar.YEAR) + }, + null, + this.id + ) + } + + private fun BaseMovie.toSearchResponse(): MovieSearchResponse { + return MovieSearchResponse( + this.title ?: this.original_title, + getUrl(id, false), + apiName, + TvType.TvSeries, + getImageUrl(this.poster_path), + this.release_date?.let { + Calendar.getInstance().apply { + time = it + }.get(Calendar.YEAR) + }, + this.id, + ) + } + + private fun TvShow.toLoadResponse(): TvSeriesLoadResponse { + val episodes = this.seasons?.mapNotNull { + it.episodes?.map { + TvSeriesEpisode( + it.name, + it.season_number, + it.episode_number, + TmdbLink( + it.external_ids?.imdb_id, + it.id, + it.episode_number, + it.season_number, + ).toJson(), + getImageUrl(it.still_path), + it.air_date?.toString(), + it.rating, + it.overview, + ) + } ?: (1..(it.episode_count ?: 1)).map { episodeNum -> + TvSeriesEpisode( + episode = episodeNum, + data = episodeNum.toString(), + season = it.season_number + ) + } + }?.flatten() ?: listOf() + + return TvSeriesLoadResponse( + this.name ?: this.original_name, + getUrl(id, true), + this@TmdbProvider.apiName, + TvType.TvSeries, + episodes, + getImageUrl(this.poster_path), + this.first_air_date?.let { + Calendar.getInstance().apply { + time = it + }.get(Calendar.YEAR) + }, + this.overview, + null,//this.status + null, // possible to get + this.rating, + this.genres?.mapNotNull { it.name }, + null, //this.episode_run_time.average() + null, + this.recommendations?.results?.map { it.toSearchResponse() } + ) + } + + private fun Movie.toLoadResponse(): MovieLoadResponse { + println("EXTERNAL IDS ${this.toJson()}") + return MovieLoadResponse( + this.title ?: this.original_title, + getUrl(id, true), + this@TmdbProvider.apiName, + TvType.Movie, + TmdbLink( + this.imdb_id, + this.id, + null, + null, + ).toJson(), + getImageUrl(this.poster_path), + this.release_date?.let { + Calendar.getInstance().apply { + time = it + }.get(Calendar.YEAR) + }, + this.overview, + null,//this.status + this.rating, + this.genres?.mapNotNull { it.name }, + null, //this.episode_run_time.average() + null, + this.recommendations?.results?.map { it.toSearchResponse() } + ) + } + + override fun getMainPage(): HomePageResponse { + + // SAME AS DISCOVER IT SEEMS +// val popularSeries = tmdb.tvService().popular(1, "en-US").execute().body()?.results?.map { +// it.toSearchResponse() +// } ?: listOf() +// +// val popularMovies = +// tmdb.moviesService().popular(1, "en-US", "840").execute().body()?.results?.map { +// it.toSearchResponse() +// } ?: listOf() + + val discoverMovies = tmdb.discoverMovie().build().execute().body()?.results?.map { + it.toSearchResponse() + } ?: listOf() + + val discoverSeries = tmdb.discoverTv().build().execute().body()?.results?.map { + it.toSearchResponse() + } ?: listOf() + + // https://en.wikipedia.org/wiki/ISO_3166-1 + val topMovies = + tmdb.moviesService().topRated(1, "en-US", "US").execute().body()?.results?.map { + it.toSearchResponse() + } ?: listOf() + + val topSeries = tmdb.tvService().topRated(1, "en-US").execute().body()?.results?.map { + it.toSearchResponse() + } ?: listOf() + + return HomePageResponse( + listOf( +// HomePageList("Popular Series", popularSeries), +// HomePageList("Popular Movies", popularMovies), + HomePageList("Popular Movies", discoverMovies), + HomePageList("Popular Series", discoverSeries), + HomePageList("Top Movies", topMovies), + HomePageList("Top Series", topSeries), + ) + ) + } + + open fun loadFromImdb(imdb: String, seasons: List): LoadResponse? { + return null + } + + open fun loadFromTmdb(tmdb: Int, seasons: List): LoadResponse? { + return null + } + + open fun loadFromImdb(imdb: String): LoadResponse? { + return null + } + + open fun loadFromTmdb(tmdb: Int): LoadResponse? { + return null + } + + // Possible to add recommendations and such here. + override fun load(url: String): LoadResponse? { + // https://www.themoviedb.org/movie/7445-brothers + // https://www.themoviedb.org/tv/71914-the-wheel-of-time + + val idRegex = Regex("""themoviedb\.org/(.*)/(\d+)""") + val found = idRegex.find(url) + + val isTvSeries = found?.groupValues?.getOrNull(1).equals("tv", ignoreCase = true) + val id = found?.groupValues?.getOrNull(2)?.toIntOrNull() ?: return null + + return if (useMetaLoadResponse) { + return if (isTvSeries) { + val body = tmdb.tvService().tv(id, "en-US").execute().body() + body?.toLoadResponse() + } else { + val body = tmdb.moviesService().summary(id, "en-US").execute().body() + body?.toLoadResponse() + } + } else { + loadFromTmdb(id)?.let { return it } + if (isTvSeries) { + tmdb.tvService().externalIds(id, "en-US").execute().body()?.imdb_id?.let { + val fromImdb = loadFromImdb(it) + val result = if (fromImdb == null) { + val details = tmdb.tvService().tv(id, "en-US").execute().body() + loadFromImdb(it, details?.seasons ?: listOf()) + ?: loadFromTmdb(id, details?.seasons ?: listOf()) + } else { + fromImdb + } + + result + } + } else { + tmdb.moviesService().externalIds(id, "en-US").execute() + .body()?.imdb_id?.let { loadFromImdb(it) } + } + } + + + } + + override fun search(query: String): List? { + return tmdb.searchService().multi(query, 1, "en-Us", "US", true).execute() + .body()?.results?.mapNotNull { + it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersTwoProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersTwoProvider.kt new file mode 100644 index 00000000..6948da08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/TrailersTwoProvider.kt @@ -0,0 +1,174 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.mapper +import com.lagradost.cloudstream3.metaproviders.TmdbProvider +import com.lagradost.cloudstream3.network.get +import com.lagradost.cloudstream3.network.text +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class TrailersTwoProvider : TmdbProvider() { + + val user = "cloudstream" + + override val apiName: String + get() = "Trailers.to" + + override val name: String + get() = "Trailers.to" + + override val mainUrl: String + get() = "https://trailers.to" + + override val useMetaLoadResponse: Boolean + get() = true + + override val instantLinkLoading: Boolean + get() = true + + override fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val mappedData = mapper.readValue(data) + println("MAPPED $mappedData") + if (mappedData.imdbID == null) return false + + val isMovie = mappedData.episode == null && mappedData.season == null + val subtitleUrl = if (isMovie) { + callback.invoke( + ExtractorLink( + this.name, + this.name, + "https://trailers.to/video/$user/imdb/${mappedData.imdbID}", + "https://trailers.to", + Qualities.Unknown.value, + false, + ) + ) + "https://trailers.to/subtitles/$user/imdb/${mappedData.imdbID}" + } else { + callback.invoke( + ExtractorLink( + this.name, + this.name, + "https://trailers.to/video/$user/imdb/${mappedData.imdbID}/S${mappedData.season ?: 1}E${mappedData.episode ?: 1}", + "https://trailers.to", + Qualities.Unknown.value, + false, + ) + ) + "https://trailers.to/subtitles/$user/imdb/${mappedData.imdbID}/S${mappedData.season ?: 1}E${mappedData.episode ?: 1}" + } + + val subtitles = + get(subtitleUrl).text + val subtitlesMapped = mapper.readValue>(subtitles) + subtitlesMapped.forEach { + subtitleCallback.invoke( + SubtitleFile( + it.LanguageCode ?: "en", + "https://trailers.to/subtitles/${it.ContentHash ?: return@forEach}/${it.LanguageCode ?: return@forEach}.vtt" // ${it.MetaInfo?.SubFormat ?: "srt"}" + ).also { println(it) } + ) + } + return true + } +} + +// Auto generated +data class TrailersSubtitleFile( + @JsonProperty("SubtitleID") val SubtitleID: Int?, + @JsonProperty("ItemID") val ItemID: Int?, + @JsonProperty("ContentText") val ContentText: String?, + @JsonProperty("ContentHash") val ContentHash: String?, + @JsonProperty("LanguageCode") val LanguageCode: String?, + @JsonProperty("MetaInfo") val MetaInfo: MetaInfo?, + @JsonProperty("EntryDate") val EntryDate: String?, + @JsonProperty("ItemSubtitleAdaptations") val ItemSubtitleAdaptations: List?, + @JsonProperty("ReleaseNames") val ReleaseNames: List?, + @JsonProperty("SubFileNames") val SubFileNames: List?, + @JsonProperty("Framerates") val Framerates: List?, + @JsonProperty("IsRelevant") val IsRelevant: Boolean? +) + +data class QueryParameters( + @JsonProperty("imdbid") val imdbid: String? +) + +data class MetaInfo( + @JsonProperty("MatchedBy") val MatchedBy: String?, + @JsonProperty("IDSubMovieFile") val IDSubMovieFile: String?, + @JsonProperty("MovieHash") val MovieHash: String?, + @JsonProperty("MovieByteSize") val MovieByteSize: String?, + @JsonProperty("MovieTimeMS") val MovieTimeMS: String?, + @JsonProperty("IDSubtitleFile") val IDSubtitleFile: String?, + @JsonProperty("SubFileName") val SubFileName: String?, + @JsonProperty("SubActualCD") val SubActualCD: String?, + @JsonProperty("SubSize") val SubSize: String?, + @JsonProperty("SubHash") val SubHash: String?, + @JsonProperty("SubLastTS") val SubLastTS: String?, + @JsonProperty("SubTSGroup") val SubTSGroup: String?, + @JsonProperty("InfoReleaseGroup") val InfoReleaseGroup: String?, + @JsonProperty("InfoFormat") val InfoFormat: String?, + @JsonProperty("InfoOther") val InfoOther: String?, + @JsonProperty("IDSubtitle") val IDSubtitle: String?, + @JsonProperty("UserID") val UserID: String?, + @JsonProperty("SubLanguageID") val SubLanguageID: String?, + @JsonProperty("SubFormat") val SubFormat: String?, + @JsonProperty("SubSumCD") val SubSumCD: String?, + @JsonProperty("SubAuthorComment") val SubAuthorComment: String?, + @JsonProperty("SubAddDate") val SubAddDate: String?, + @JsonProperty("SubBad") val SubBad: String?, + @JsonProperty("SubRating") val SubRating: String?, + @JsonProperty("SubSumVotes") val SubSumVotes: String?, + @JsonProperty("SubDownloadsCnt") val SubDownloadsCnt: String?, + @JsonProperty("MovieReleaseName") val MovieReleaseName: String?, + @JsonProperty("MovieFPS") val MovieFPS: String?, + @JsonProperty("IDMovie") val IDMovie: String?, + @JsonProperty("IDMovieImdb") val IDMovieImdb: String?, + @JsonProperty("MovieName") val MovieName: String?, + @JsonProperty("MovieNameEng") val MovieNameEng: String?, + @JsonProperty("MovieYear") val MovieYear: String?, + @JsonProperty("MovieImdbRating") val MovieImdbRating: String?, + @JsonProperty("SubFeatured") val SubFeatured: String?, + @JsonProperty("UserNickName") val UserNickName: String?, + @JsonProperty("SubTranslator") val SubTranslator: String?, + @JsonProperty("ISO639") val ISO639: String?, + @JsonProperty("LanguageName") val LanguageName: String?, + @JsonProperty("SubComments") val SubComments: String?, + @JsonProperty("SubHearingImpaired") val SubHearingImpaired: String?, + @JsonProperty("UserRank") val UserRank: String?, + @JsonProperty("SeriesSeason") val SeriesSeason: String?, + @JsonProperty("SeriesEpisode") val SeriesEpisode: String?, + @JsonProperty("MovieKind") val MovieKind: String?, + @JsonProperty("SubHD") val SubHD: String?, + @JsonProperty("SeriesIMDBParent") val SeriesIMDBParent: String?, + @JsonProperty("SubEncoding") val SubEncoding: String?, + @JsonProperty("SubAutoTranslation") val SubAutoTranslation: String?, + @JsonProperty("SubForeignPartsOnly") val SubForeignPartsOnly: String?, + @JsonProperty("SubFromTrusted") val SubFromTrusted: String?, + @JsonProperty("QueryCached") val QueryCached: Int?, + @JsonProperty("SubTSGroupHash") val SubTSGroupHash: String?, + @JsonProperty("SubDownloadLink") val SubDownloadLink: String?, + @JsonProperty("ZipDownloadLink") val ZipDownloadLink: String?, + @JsonProperty("SubtitlesLink") val SubtitlesLink: String?, + @JsonProperty("QueryNumber") val QueryNumber: String?, + @JsonProperty("QueryParameters") val QueryParameters: QueryParameters?, + @JsonProperty("Score") val Score: Double? +) + +data class ItemSubtitleAdaptations( + @JsonProperty("ContentHash") val ContentHash: String?, + @JsonProperty("OffsetMs") val OffsetMs: Int?, + @JsonProperty("Framerate") val Framerate: Int?, + @JsonProperty("Views") val Views: Int?, + @JsonProperty("EntryDate") val EntryDate: String?, + @JsonProperty("Subtitle") val Subtitle: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 53eaed4e..9b88abbf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -51,7 +51,10 @@ object AppUtils { intent.data = Uri.parse(url) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - startActivity(Intent.createChooser(intent, null).putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)) + startActivity( + Intent.createChooser(intent, null) + .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components) + ) else startActivity(intent) } @@ -68,6 +71,11 @@ object AppUtils { return queryPairs } + /** Any object as json string */ + fun Any.toJson(): String { + return mapper.writeValueAsString(this) + } + /**| S1:E2 Hello World * | Episode 2. Hello world * | Hello World @@ -103,7 +111,12 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() - fun AppCompatActivity.loadResult(url: String, apiName: String, startAction: Int = 0, startValue: Int = 0) { + fun AppCompatActivity.loadResult( + url: String, + apiName: String, + startAction: Int = 0, + startValue: Int = 0 + ) { this.runOnUiThread { // viewModelStore.clear() this.navigate( @@ -113,7 +126,11 @@ object AppUtils { } } - fun Activity?.loadSearchResult(card: SearchResponse, startAction: Int = 0, startValue: Int = 0) { + fun Activity?.loadSearchResult( + card: SearchResponse, + startAction: Int = 0, + startValue: Int = 0 + ) { (this as AppCompatActivity?)?.loadResult(card.url, card.apiName, startAction, startValue) } @@ -179,7 +196,8 @@ object AppUtils { val conManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkInfo = conManager.allNetworks return networkInfo.any { - conManager.getNetworkCapabilities(it)?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true + conManager.getNetworkCapabilities(it) + ?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true } } @@ -216,7 +234,10 @@ object AppUtils { return currentAudioFocusRequest } - fun filterProviderByPreferredMedia(apis: ArrayList, currentPrefMedia: Int): List { + fun filterProviderByPreferredMedia( + apis: ArrayList, + currentPrefMedia: Int + ): List { val allApis = apis.filter { api -> api.hasMainPage } return if (currentPrefMedia < 1) { allApis @@ -226,7 +247,8 @@ object AppUtils { val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon) val mediaTypeList = if (currentPrefMedia == 1) listEnumMovieTv else listEnumAnime - val filteredAPI = allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } } + val filteredAPI = + allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } } filteredAPI } }