forked from recloudstream/cloudstream
Added https://ihavenotv.com (#308)
Co-authored-by: LagradOst <balt.758@gmail.com>
This commit is contained in:
parent
743d84fc38
commit
ca6e9fb889
6 changed files with 240 additions and 3 deletions
|
@ -38,6 +38,7 @@ object APIHolder {
|
||||||
// MeloMovieProvider(), // Captcha for links
|
// MeloMovieProvider(), // Captcha for links
|
||||||
DubbedAnimeProvider(),
|
DubbedAnimeProvider(),
|
||||||
HDMProvider(),
|
HDMProvider(),
|
||||||
|
IHaveNoTvProvider(), // Documentaries provider
|
||||||
//LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...)
|
//LookMovieProvider(), // RECAPTCHA (Please allow up to 5 seconds...)
|
||||||
VMoveeProvider(),
|
VMoveeProvider(),
|
||||||
WatchCartoonOnlineProvider(),
|
WatchCartoonOnlineProvider(),
|
||||||
|
@ -272,6 +273,20 @@ fun sortSubs(urls: List<SubtitleFile>): List<SubtitleFile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 */
|
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
|
||||||
fun imdbUrlToId(url: String): String? {
|
fun imdbUrlToId(url: String): String? {
|
||||||
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
|
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
|
||||||
|
@ -315,6 +330,7 @@ enum class TvType {
|
||||||
Anime,
|
Anime,
|
||||||
ONA,
|
ONA,
|
||||||
Torrent,
|
Torrent,
|
||||||
|
Documentary,
|
||||||
}
|
}
|
||||||
|
|
||||||
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
|
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
|
||||||
|
|
|
@ -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<HomePageList>()
|
||||||
|
|
||||||
|
categories.forEach { cat ->
|
||||||
|
val link = "$mainUrl/category/$cat"
|
||||||
|
val html = app.get(link).text
|
||||||
|
val soup = Jsoup.parse(html)
|
||||||
|
|
||||||
|
val searchResults: MutableMap<String, SearchResponse> = 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<SearchResponse> {
|
||||||
|
val url = """$mainUrl/search/${URLEncoder.encode(query, "UTF-8")}"""
|
||||||
|
val response = app.get(url).text
|
||||||
|
val soup = Jsoup.parse(response)
|
||||||
|
|
||||||
|
val searchResults: MutableMap<String, SearchResponse> = 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<String> = 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<Int?, Int?>(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
|
||||||
|
}
|
||||||
|
}
|
|
@ -191,6 +191,7 @@ class HomeFragment : Fragment() {
|
||||||
val typeChoices = listOf(
|
val typeChoices = listOf(
|
||||||
Pair(R.string.movies, listOf(TvType.Movie)),
|
Pair(R.string.movies, listOf(TvType.Movie)),
|
||||||
Pair(R.string.tv_series, listOf(TvType.TvSeries)),
|
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.cartoons, listOf(TvType.Cartoon)),
|
||||||
Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
||||||
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
||||||
|
|
|
@ -463,7 +463,6 @@ class ResultFragment : Fragment() {
|
||||||
if (isMovie) null else episodeClick.data.episode
|
if (isMovie) null else episodeClick.data.episode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
val folder = when (currentType) {
|
val folder = when (currentType) {
|
||||||
TvType.Anime -> "Anime/$titleName"
|
TvType.Anime -> "Anime/$titleName"
|
||||||
TvType.Movie -> "Movies"
|
TvType.Movie -> "Movies"
|
||||||
|
@ -472,7 +471,8 @@ class ResultFragment : Fragment() {
|
||||||
TvType.ONA -> "ONA"
|
TvType.ONA -> "ONA"
|
||||||
TvType.Cartoon -> "Cartoons/$titleName"
|
TvType.Cartoon -> "Cartoons/$titleName"
|
||||||
TvType.Torrent -> "Torrent"
|
TvType.Torrent -> "Torrent"
|
||||||
else -> null
|
TvType.Documentary -> "Documentaries"
|
||||||
|
null -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
|
|
|
@ -140,7 +140,7 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
val typeChoices = listOf(
|
val typeChoices = listOf(
|
||||||
Pair(R.string.movies, listOf(TvType.Movie)),
|
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.cartoons, listOf(TvType.Cartoon)),
|
||||||
Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
Pair(R.string.anime, listOf(TvType.Anime, TvType.ONA, TvType.AnimeMovie)),
|
||||||
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
||||||
|
|
|
@ -241,11 +241,21 @@
|
||||||
<string name="used_storage">Used</string>
|
<string name="used_storage">Used</string>
|
||||||
<string name="app_storage">App</string>
|
<string name="app_storage">App</string>
|
||||||
|
|
||||||
|
<!--plural-->
|
||||||
<string name="movies">Movies</string>
|
<string name="movies">Movies</string>
|
||||||
<string name="tv_series">TvSeries</string>
|
<string name="tv_series">TvSeries</string>
|
||||||
<string name="cartoons">Cartoons</string>
|
<string name="cartoons">Cartoons</string>
|
||||||
<string name="anime">Anime</string>
|
<string name="anime">Anime</string>
|
||||||
<string name="torrent">Torrent</string>
|
<string name="torrent">Torrent</string>
|
||||||
|
<string name="documentaries">Documentaries</string>
|
||||||
|
|
||||||
|
<!--singular-->
|
||||||
|
<string name="movies_singular">@string/movies</string>
|
||||||
|
<string name="tv_series_singular">Series</string>
|
||||||
|
<string name="cartoons_singular">Cartoon</string>
|
||||||
|
<string name="anime_singular">@string/anime</string>
|
||||||
|
<string name="torrent_singular">@string/torrent</string>
|
||||||
|
<string name="documentaries_singular">Documentary</string>
|
||||||
|
|
||||||
<string name="source_error">Source error</string>
|
<string name="source_error">Source error</string>
|
||||||
<string name="remote_error">Remote error</string>
|
<string name="remote_error">Remote error</string>
|
||||||
|
|
Loading…
Reference in a new issue