From ca6e9fb8898849c72e21f48e8ba58d0803363f80 Mon Sep 17 00:00:00 2001 From: Arjix <53124886+ArjixWasTaken@users.noreply.github.com> Date: Thu, 9 Dec 2021 01:41:26 +0200 Subject: [PATCH] Added https://ihavenotv.com (#308) Co-authored-by: LagradOst --- .../com/lagradost/cloudstream3/MainAPI.kt | 16 ++ .../movieproviders/IHaveNoTvProvider.kt | 210 ++++++++++++++++++ .../cloudstream3/ui/home/HomeFragment.kt | 1 + .../cloudstream3/ui/result/ResultFragment.kt | 4 +- .../cloudstream3/ui/search/SearchFragment.kt | 2 +- app/src/main/res/values/strings.xml | 10 + 6 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/movieproviders/IHaveNoTvProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index b915f612..09a1a110 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -38,6 +38,7 @@ object APIHolder { // MeloMovieProvider(), // Captcha for links DubbedAnimeProvider(), HDMProvider(), + IHaveNoTvProvider(), // Documentaries provider //LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...) VMoveeProvider(), WatchCartoonOnlineProvider(), @@ -272,6 +273,20 @@ fun sortSubs(urls: List): List { } } +fun capitalizeString(str: String): String { + return capitalizeStringNullable(str) ?: str +} + +fun capitalizeStringNullable(str: String?): String? { + if (str == null) + return null + return try { + str.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + } catch (e: Exception) { + str + } +} + /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ fun imdbUrlToId(url: String): String? { return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1) @@ -315,6 +330,7 @@ enum class TvType { Anime, ONA, Torrent, + Documentary, } // IN CASE OF FUTURE ANIME MOVIE OR SMTH diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/IHaveNoTvProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/IHaveNoTvProvider.kt new file mode 100644 index 00000000..91d7460d --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/IHaveNoTvProvider.kt @@ -0,0 +1,210 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.StreamTape +import com.lagradost.cloudstream3.utils.ExtractorLink +import org.jsoup.Jsoup +import java.net.URLEncoder + +class IHaveNoTvProvider : MainAPI() { + override val mainUrl = "https://ihavenotv.com" + override val name = "I Have No TV" + override val hasQuickSearch = false + override val hasMainPage = true + + override val supportedTypes = setOf(TvType.Documentary) + + override fun getMainPage(): HomePageResponse { + // Uhh, I am too lazy to scrape the "latest documentaries" and "recommended documentaries", + // so I am just scraping 3 random categories + val allCategories = listOf( + "astronomy", "brain", "creativity", "design", "economics", "environment", "health", "history", + "lifehack", "math", "music", "nature", "people", "physics", "science", "technology", "travel" + ) + + val categories = allCategories.asSequence().shuffled().take(3) + .toList() // randomly get 3 categories, because there are too many + + val items = ArrayList() + + categories.forEach { cat -> + val link = "$mainUrl/category/$cat" + val html = app.get(link).text + val soup = Jsoup.parse(html) + + val searchResults: MutableMap = mutableMapOf() + soup.select(".episodesDiv .episode").forEach { res -> + val poster = res.selectFirst("img")?.attr("src") + val aTag = if (res.html().contains("/series/")) { + res.selectFirst(".episodeMeta > a") + } else { + res.selectFirst("a[href][title]") + } + val year = Regex("""•?\s+(\d{4})\s+•""").find( + res.selectFirst(".episodeMeta").text() + )?.destructured?.component1()?.toIntOrNull() + + val title = aTag.attr("title") + val href = fixUrl(aTag.attr("href")) + searchResults[href] = TvSeriesSearchResponse( + title, + href, + this.name, + TvType.Documentary,//if (href.contains("/series/")) TvType.TvSeries else TvType.Movie, + poster, + year, + null + ) + } + items.add( + HomePageList( + capitalizeString(cat), + ArrayList(searchResults.values).subList(0, 5) + ) + ) // just 5 results per category, app crashes when they are too many + } + + return HomePageResponse(items) + } + + override fun search(query: String): ArrayList { + val url = """$mainUrl/search/${URLEncoder.encode(query, "UTF-8")}""" + val response = app.get(url).text + val soup = Jsoup.parse(response) + + val searchResults: MutableMap = mutableMapOf() + + soup.select(".episodesDiv .episode").forEach { res -> + val poster = res.selectFirst("img")?.attr("src") + val aTag = if (res.html().contains("/series/")) { + res.selectFirst(".episodeMeta > a") + } else { + res.selectFirst("a[href][title]") + } + val year = + Regex("""•?\s+(\d{4})\s+•""").find(res.selectFirst(".episodeMeta").text())?.destructured?.component1() + ?.toIntOrNull() + + val title = aTag.attr("title") + val href = fixUrl(aTag.attr("href")) + searchResults[href] = TvSeriesSearchResponse( + title, + href, + this.name, + TvType.Documentary, //if (href.contains("/series/")) TvType.TvSeries else TvType.Movie, + poster, + year, + null + ) + } + + return ArrayList(searchResults.values) + } + + override fun load(url: String): LoadResponse { + val isSeries = url.contains("/series/") + val html = app.get(url).text + val soup = Jsoup.parse(html) + + val container = soup.selectFirst(".container-fluid h1")?.parent() + val title = if (isSeries) { + container?.selectFirst("h1")?.text()?.split("•")?.firstOrNull().toString() + } else soup.selectFirst(".videoDetails").selectFirst("strong")?.text().toString() + val description = if (isSeries) { + container?.selectFirst("p")?.text() + } else { + soup.selectFirst(".videoDetails > p")?.text() + } + + var year: Int? = null + val categories: MutableSet = mutableSetOf() + + val episodes = if (isSeries) { + container?.select(".episode")?.map { ep -> + val thumb = ep.selectFirst("img").attr("src") + val epTitle = ep.selectFirst("a[title]").attr("title") + val epLink = fixUrl(ep.selectFirst("a[title]").attr("href")) + val (season, epNum) = if (ep.selectFirst(".episodeMeta > strong") != null && + ep.selectFirst(".episodeMeta > strong").html().contains("S") + ) { + val split = ep.selectFirst(".episodeMeta > strong")?.text()?.split("E") + Pair( + split?.firstOrNull()?.replace("S", "")?.toIntOrNull(), + split?.get(1)?.toIntOrNull() + ) + } else Pair(null, null) + val epDescription = ep.selectFirst(".episodeSynopsis")?.text() + year = Regex("""•?\s+(\d{4})\s+•""").find( + ep.selectFirst(".episodeMeta").text() + )?.destructured?.component1()?.toIntOrNull() + + categories.addAll(ep.select(".episodeMeta > a[href*=\"/category/\"]").map { it.text().trim() }) + + TvSeriesEpisode( + epTitle, + season, + epNum, + epLink, + thumb, + null, + null, + epDescription + ) + } + } else { + listOf(MovieLoadResponse( + title, + url, + this.name, + TvType.Movie, + url, + soup.selectFirst("[rel=\"image_src\"]").attr("href"), + Regex("""•?\s+(\d{4})\s+•""").find( + soup.selectFirst(".videoDetails").text() + )?.destructured?.component1()?.toIntOrNull(), + description, + null, + null, + soup.selectFirst(".videoDetails").select("a[href*=\"/category/\"]").map { it.text().trim() } + )) + } + + val poster = episodes?.firstOrNull().let { + if (isSeries && it != null) (it as TvSeriesEpisode).posterUrl + else null + } + + return if (isSeries) TvSeriesLoadResponse( + title, + url, + this.name, + TvType.TvSeries, + episodes!!.map { it as TvSeriesEpisode }, + poster, + year, + description, + null, + null, + null, + categories.toList() + ) else (episodes?.first() as MovieLoadResponse) + } + + override fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val html = app.get(data).text + val soup = Jsoup.parse(html) + + val iframe = soup.selectFirst("#videoWrap iframe") + if (iframe != null) { + if (iframe.attr("src").startsWith("https://streamtape.com")) { + StreamTape().getSafeUrl(iframe.attr("src"))?.forEach(callback) + } + } + return true + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 937832be..878970d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -191,6 +191,7 @@ class HomeFragment : Fragment() { val typeChoices = listOf( Pair(R.string.movies, listOf(TvType.Movie)), Pair(R.string.tv_series, listOf(TvType.TvSeries)), + Pair(R.string.documentaries, listOf(TvType.Documentary)), Pair(R.string.cartoons, listOf(TvType.Cartoon)), Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)), Pair(R.string.torrent, listOf(TvType.Torrent)), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index a0b058c5..8903b5f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -463,7 +463,6 @@ class ResultFragment : Fragment() { if (isMovie) null else episodeClick.data.episode ) - val folder = when (currentType) { TvType.Anime -> "Anime/$titleName" TvType.Movie -> "Movies" @@ -472,7 +471,8 @@ class ResultFragment : Fragment() { TvType.ONA -> "ONA" TvType.Cartoon -> "Cartoons/$titleName" TvType.Torrent -> "Torrent" - else -> null + TvType.Documentary -> "Documentaries" + null -> null } context?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index e1f95340..c49e3be5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -140,7 +140,7 @@ class SearchFragment : Fragment() { val typeChoices = listOf( Pair(R.string.movies, listOf(TvType.Movie)), - Pair(R.string.tv_series, listOf(TvType.TvSeries)), + Pair(R.string.tv_series, listOf(TvType.TvSeries, TvType.Documentary)), Pair(R.string.cartoons, listOf(TvType.Cartoon)), Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)), Pair(R.string.torrent, listOf(TvType.Torrent)), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd38c149..f6e25cad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -241,11 +241,21 @@ Used App + Movies TvSeries Cartoons Anime Torrent + Documentaries + + + @string/movies + Series + Cartoon + @string/anime + @string/torrent + Documentary Source error Remote error