diff --git a/.github/ISSUE_TEMPLATE/-bug--short-title.md b/.github/ISSUE_TEMPLATE/-bug--short-title.md deleted file mode 100644 index 1fb6c56c..00000000 --- a/.github/ISSUE_TEMPLATE/-bug--short-title.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: "[Bug] Short Title" -about: 'Report any bugs or errors found on App. ' -title: '' -labels: '' -assignees: '' - ---- - -**Guidelines:** -- [ ] It **is not** a duplicate issue. -- [ ] If related to a provider, I have checked the site and it works, but not the app. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots or video** -If applicable, add screenshots to help explain your problem. - -**Device (please complete the following information):** - - Android Version: [e.g. Android 11 API 30, Android TV] - - App Version: [e.g. v2.7.14] - - Provider: [e.g. https://trailers.to/en/movie/4391225/red-notice-2021] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/-feature-request--title.md b/.github/ISSUE_TEMPLATE/-feature-request--title.md deleted file mode 100644 index c6d5b844..00000000 --- a/.github/ISSUE_TEMPLATE/-feature-request--title.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: "[Feature Request] Title" -about: 'Suggest enhancement for the App. ' -title: '' -labels: '' -assignees: '' - ---- - -**Guidelines:** -- [ ] It **is not** a duplicate issue. -- [ ] It **is not** a request to add a website. Write it here instead https://github.com/LagradOst/CloudStream-3/issues/24 - -**Choose one of the following:** -- UI/UX enhancement -- New feature -- Other - -**Describe your request here** *(fill this up)* -- [ ] How it works. -- [ ] Issues it solves. -- [ ] Why it is needed. - -**Other related info** diff --git a/.github/ISSUE_TEMPLATE/application-bug.yml b/.github/ISSUE_TEMPLATE/application-bug.yml new file mode 100644 index 00000000..6357354b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/application-bug.yml @@ -0,0 +1,83 @@ +name: 🐞 Application Issue Report +description: Report a issue in CloudStream +labels: [bug] +body: + + - type: textarea + id: reproduce-steps + attributes: + label: Steps to reproduce + description: Provide an example of the issue. + placeholder: | + Example: + 1. First step + 2. Second step + 3. Issue here + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + placeholder: | + Example: + "This should happen..." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual behavior + placeholder: | + Example: + "This happened instead..." + validations: + required: true + + - type: input + id: cloudstream-version + attributes: + label: Cloudstream version + description: | + You can find your Cloudstream version in **Settings**. + placeholder: | + Example: "2.8.16" + validations: + required: true + + - type: input + id: android-version + attributes: + label: Android version + description: | + You can find this somewhere in your Android settings. + placeholder: | + Example: "Android 12" + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other details + placeholder: | + Additional details and attachments. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if you haven't done these steps. + options: + - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. + required: true + - label: I have written a short but informative title. + required: true + - label: I have updated the app to pre-release version **[Latest](https://github.com/LagradOst/CloudStream-3/releases)**. + required: true + - label: If related to a provider, I have checked the site and it works, but not the app. + required: true + - label: I will fill out all of the requested information in this form. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..9bc7ddb3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,35 @@ +name: ⭐ Feature request +description: Suggest a feature to improve the app +labels: [feature request] +body: + + - type: textarea + id: feature-description + attributes: + label: Describe your suggested feature + description: How can an existing source be improved? + placeholder: | + Example: + "It should work like this..." + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other details + placeholder: | + Additional details and attachments. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if you haven't done these steps. + options: + - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. + required: true + - label: I have written a short but informative title. + required: true + - label: I will fill out all of the requested information in this form. + required: true diff --git a/.github/ISSUE_TEMPLATE/provider-bug.yml b/.github/ISSUE_TEMPLATE/provider-bug.yml new file mode 100644 index 00000000..3b5a751e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/provider-bug.yml @@ -0,0 +1,84 @@ +name: 🐞 Provider Issue Report +description: Report a source issue +labels: [provider] +body: + + - type: input + id: source + attributes: + label: Source information + description: | + You can find the source name in navigation drawer. + placeholder: | + Example: "Bflix" + validations: + required: true + + - type: input + id: source-url + attributes: + label: Source link + placeholder: | + Example: + "www.example.org" + validations: + required: true + + - type: textarea + id: reproduce-steps + attributes: + label: Steps to reproduce + description: Provide an example of the issue. + placeholder: | + Example: + 1. First step + 2. Second step + 3. Issue here + validations: + required: true + + - type: input + id: cloudstream-version + attributes: + label: CloudStream version + description: | + You can find your CloudStream version in **Settings**. + placeholder: | + Example: "2.8.16" + validations: + required: true + + - type: input + id: android-version + attributes: + label: Android version + description: | + You can find this somewhere in your Android settings. + placeholder: | + Example: "Android 12" + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other details + placeholder: | + Additional details and attachments. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if you haven't done these steps. + options: + - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. + required: true + - label: I have written a short but informative title. + required: true + - label: I have updated the app to pre-release version **[Latest](https://github.com/LagradOst/CloudStream-3/releases)**. + required: true + - label: If related to a provider, I have checked the site and it works, but not the app. + required: true + - label: I will fill out all of the requested information in this form. + required: true diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index a3426106..511b2d83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -102,6 +102,7 @@ object APIHolder { TenshiProvider(), WcoProvider(), AnimePaheProvider(), + DreamSubProvider(), NineAnimeProvider(), AnimeWorldProvider(), ZoroProvider(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DreamSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DreamSubProvider.kt new file mode 100644 index 00000000..725e1054 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DreamSubProvider.kt @@ -0,0 +1,263 @@ +package com.lagradost.cloudstream3.animeproviders + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration +import com.lagradost.cloudstream3.LoadResponse.Companion.addRating +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getQualityFromName +import org.jsoup.nodes.Element + +class DreamSubProvider : MainAPI() { + override var mainUrl = "https://dreamsub.me" + override var name = "DreamSub" + override val lang = "it" + override val hasMainPage = true + + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + companion object { + fun getType(t: String?): TvType { + return when (t?.lowercase()) { + "film" -> TvType.AnimeMovie + "ova" -> TvType.OVA + else -> TvType.Anime + } + } + + fun getStatus(t: String?): ShowStatus? { + return when (t?.lowercase()) { + "conclusa" -> ShowStatus.Completed + "in corso" -> ShowStatus.Ongoing + else -> null + } + } + } + + private fun Element.toSearchResponse(): AnimeSearchResponse { + val title = this.select(".title").first()?.text() + ?: throw ErrorLoadingException("No title found") + val url = this.select(".showStreaming a").first()?.attr("href") + ?: throw ErrorLoadingException("No url found") + + val type = TvType.Anime + val posterUrl = this.select(".cover").first()?.attr("style") + ?.substringAfter("url(")?.substringBefore(")")?.trim() + + val dubInfo = this.select(".showStreaming").first()?.text() + ?.substringAfter("Lingua:")?.substringBefore("\n")?.trim() + val dub = dubInfo?.contains("DUB ITA", ignoreCase = true) ?: false + val sub = dubInfo?.contains("SUB ITA", ignoreCase = true) ?: true + + val desc = this.select(".desc").text() + val episodes = desc.substringAfter("Episodi:").substringBefore(",") + .removeSuffix("+").trim().toIntOrNull() + val year = desc.substringAfter("Anno di inizio:").substringBefore(",") + .trim().toIntOrNull() + + return newAnimeSearchResponse(title, url, type) { + addDubStatus(dub, sub, episodes, episodes) + this.posterUrl = fixUrlNull(posterUrl) + this.year = year + } + } + + private fun Element.episodeToSearchResponse(): AnimeSearchResponse { + val title = this.select(".item-title a").first()?.text() + ?: throw ErrorLoadingException("No title found") + val url = fixUrlNull(this.select("a").first()?.attr("href")) + ?: throw ErrorLoadingException("No url found") + + val type = getType(this.select(".gr-eps").first()?.text()) + val posterUrl = this.select("img").first()?.attr("src") + val episodes = this.select("div[style]").first()?.text() + ?.replace("Ep", "")?.trim()?.toIntOrNull() + + val dubInfo = this.select(".grt-dub").first()?.text() + val dub = dubInfo?.contains("DUB ITA", ignoreCase = true) ?: false + val sub = dubInfo?.contains("SUB ITA", ignoreCase = true) ?: true + + return newAnimeSearchResponse(title, url, type) { + addDubStatus(dub, sub, episodes, episodes) + this.posterUrl = fixUrlNull(posterUrl) + } + } + + private fun Element.toEpisode(season: Int? = null): Episode? { + val available = this.select(".sli-btn").first() != null + if (!available) + return null + + val anchor = this.select(".sli-name a") + val href = anchor.attr("href") + val name = anchor.text().substringAfter(":").trim() + val episode = anchor.text().substringAfter("Episodio").substringBefore(":") + .trim().toIntOrNull() + + val date = this.select(".sli-name span").text() + + return newEpisode(href) { + this.name = if (name != "TBA") name else null + this.season = season + this.episode = episode + addDate(date, "dd MMMM yyyy") + } + } + + override suspend fun getMainPage(): HomePageResponse { + val document = app.get(mainUrl).document + val list = ArrayList() + + val episodeList = document.select("#episodiRecenti ul.grid-item li").map { + it.episodeToSearchResponse() + } + list.add(HomePageList("Ultimi Episodi", episodeList)) + + val updatedList = document.select("#episodiNuovi ul.grid-item li").map { + it.episodeToSearchResponse() + } + list.add(HomePageList("Ultimi Aggiornamenti", updatedList)) + + val recommendedList = document.select("#sliderEvidenza .tvBlock").map { + it.toSearchResponse() + } + list.add(HomePageList("In Evidenza", recommendedList)) + + val lastedList = document.select("#sliderUltimeInserite .tvBlock").map { + it.toSearchResponse() + } + list.add(HomePageList("Ultime Inserite", lastedList)) + return HomePageResponse(list) + } + + override suspend fun search(query: String): List { + val document = app.get("$mainUrl/search/?q=$query").document + return document.select(".tvBlock").map { + it.toSearchResponse() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val details = document.select("#animeDetails").first() + ?: throw ErrorLoadingException("Not found") + + val title = details.select(".dc-title").text() + val plot = document.select("#tramaLong").first()?.text() + ?: document.select(".dci-desc").first()?.text() + val posterUrl = fixUrl(details.select(".dc-thumb img").attr("src")) + + val rating = document.select("#vote_percent").text() + val trailerUrl = document.select("#media-play iframe").attr("data-url") + + var type: TvType = TvType.Anime + var duration: String? = null + var dub = false + var tags: List = listOf() + var year: Int? = null + var status: ShowStatus? = null + var japName: String? = null + + for (meta in document.select(".dci-spe .dcis")) { + val key = meta.select("b").text() + val value = meta.text().removePrefix(key).trim() + when (key) { + "Tipo:" -> type = getType(value) + "Durata:" -> duration = value + "Lingua:" -> dub = value.contains("DUB ITA", ignoreCase = true) + "Genere:" -> tags = meta.select("a").map { it.text() } + "Data:" -> { + val values = value.split(",") + year = values[0].substringBefore(" a ").trim() + .split(" ").getOrNull(2)?.toIntOrNull() + status = getStatus(values.getOrNull(1)?.trim()) + } + // "Titolo giapponese:" -> japName = value + "Altri titoli:" -> japName = value + } + } + + val episodes = mutableListOf() + val seasonElements = document.select("#episodes-sv > li.mainSeas") + val episodeElements = document.select("#episodes-sv li.ep-item") + when { + seasonElements.isNotEmpty() -> { + seasonElements.forEachIndexed { index, element -> + val season = index + 1 + element.select("li.ep-item").forEach { + val episode = it.toEpisode(season) + if (episode != null) + episodes.add(episode) + } + } + } + episodeElements.isNotEmpty() -> { + episodeElements.forEach { + val episode = it.toEpisode() + if (episode != null) + episodes.add(episode) + } + } + else -> episodes.add(newEpisode(url)) // Movie or Special + } + + val recommendations = document.select(".related-list ul.grid-item li").map { + it.episodeToSearchResponse() + } + val comingSoon = episodes.isEmpty() + + return newAnimeLoadResponse(title, url, type) { + addEpisodes(if (dub) DubStatus.Dubbed else DubStatus.Subbed, episodes) + addPoster(posterUrl) + addRating(rating) + addDuration(duration) + addTrailer(trailerUrl) + this.japName = japName + this.year = year + this.showStatus = status + this.plot = plot + this.tags = tags + this.recommendations = recommendations + this.comingSoon = comingSoon + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val document = app.get(data).document + + val links = document.select("#main-content.onlyDesktop .goblock-content > div") + if (links.isEmpty()) + return false + + links.forEach { tab -> + val sub = tab.select("b").text().contains("SUB") + tab.select("a.dwButton").forEach { + val title = document.select("#current_episode_name").text() + .substringAfter(":").trim() + val url = it.attr("href") + val quality = getQualityFromName(it.text()) + callback.invoke( + ExtractorLink( + name, + (if (sub) "SUB ITA" else "DUB") + (if (title != "TBA") " - $title" else ""), + url, + referer = url, + headers = mapOf("host" to "https://cdn.dreamsub.me"), + quality = quality + ) + ) + } + } + return true + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SoaptwoDayProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SoaptwoDayProvider.kt index 3a7519f8..fe5a4b8c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SoaptwoDayProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/SoaptwoDayProvider.kt @@ -72,13 +72,23 @@ class SoaptwoDayProvider:MainAPI() { val title = soup.selectFirst(".hidden-lg > div:nth-child(1) > h4")?.text() ?: "" val description = soup.selectFirst("p#wrap")?.text()?.trim() val poster = soup.selectFirst(".col-md-5 > div:nth-child(1) > div:nth-child(1) > img")?.attr("src") - val episodes = soup.select("div.alert > div > div > a").mapNotNull { - val link = fixUrlNull(it?.attr("href")) ?: return@mapNotNull null - val name = it?.text()?.replace(Regex("(^(\\d+)\\.)"),"") - Episode( - name = name, - data = link - ) + val episodes = mutableListOf() + soup.select("div.alert").forEach { + val season = it?.selectFirst("h4")?.text()?.filter { c -> c.isDigit() }?.toIntOrNull() + it?.select("div > div > a")?.forEach { entry -> + val link = fixUrlNull(entry?.attr("href")) ?: return@forEach + val text = entry?.text() ?: "" + val name = text.replace(Regex("(^(\\d+)\\.)"),"") + val epNum = text.substring(0, text.indexOf(".")).toIntOrNull() + episodes.add( + Episode( + name = name, + data = link, + season = season, + episode = epNum + ) + ) + } } val otherInfoBody = soup.select("div.col-sm-8 div.panel-body").toString() //Fetch casts diff --git a/docs/providers.json b/docs/providers.json index d8db65bc..2ae31b8d 100644 --- a/docs/providers.json +++ b/docs/providers.json @@ -94,6 +94,11 @@ "status": 1, "url": "https://dramasee.net" }, + "DreamSubProvider": { + "name": "DreamSub", + "status": 1, + "url": "https://dreamsub.me" + }, "DubbedAnimeProvider": { "name": "DubbedAnime", "status": 1,