diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..d138a1e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -89,9 +90,9 @@ class DownloadChildFragment : Fragment() { setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index fad349c8..2019aa50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -104,7 +108,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +264,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +302,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } 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 a1574eec..1d3f5a08 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 @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 37a905a7..de339aee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1099,13 +1099,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } @@ -2277,7 +2278,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2326,7 +2328,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 274294cb..6106845e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -83,6 +83,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +140,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +177,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -187,6 +190,7 @@ import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -208,6 +212,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -890,7 +895,12 @@ val extractorApis: MutableList = arrayListOf( StreamWishExtractor(), EmturbovidExtractor(), Vtbe(), - EPlayExtractor() + EPlayExtractor(), + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers() ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + Episodes %1$d-%2$d %1$d %2$s + Upcoming in %s S E No Episodes found