diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a083008d..398a0d5e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,11 +30,24 @@
android:supportsPictureInPicture="true">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
? {
+ open fun search(query: String): List? {
return null
}
- open fun quickSearch(query: String): ArrayList? {
+ open fun quickSearch(query: String): List? {
return null
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index ba023c3b..39e08daa 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -32,8 +32,10 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.Event
+import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result.*
+import kotlin.concurrent.thread
const val VLC_PACKAGE = "org.videolan.vlc"
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
@@ -295,6 +297,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
createISO()
}*/
handleAppIntent(intent)
+
+ thread {
+ runAutoUpdate()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt
index 21d1df12..e9318f84 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/DubbedAnimeProvider.kt
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime
+import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import org.jsoup.Jsoup
@@ -17,6 +18,8 @@ class DubbedAnimeProvider : MainAPI() {
get() = "DubbedAnime"
override val hasQuickSearch: Boolean
get() = true
+ override val hasMainPage: Boolean
+ get() = true
override val supportedTypes: Set
get() = setOf(
@@ -57,6 +60,67 @@ class DubbedAnimeProvider : MainAPI() {
@JsonProperty("tags") val tags: String,*/
)
+ private fun parseDocumentTrending(url: String): List {
+ val response = khttp.get(url)
+ val document = Jsoup.parse(response.text)
+ return document.select("li > a").map {
+ val href = fixUrl(it.attr("href"))
+ val title = it.selectFirst("> div > div.cittx").text()
+ val poster = fixUrl(it.selectFirst("> div > div.imghddde > img").attr("src"))
+ AnimeSearchResponse(
+ title,
+ href,
+ this.name,
+ TvType.Anime,
+ poster,
+ null,
+ null,
+ EnumSet.of(DubStatus.Dubbed),
+ null,
+ null
+ )
+ }
+ }
+
+ private fun parseDocument(url: String, trimEpisode: Boolean = false): List {
+ val response = khttp.get(url)
+ val document = Jsoup.parse(response.text)
+ return document.select("a.grid__link").map {
+ val href = fixUrl(it.attr("href"))
+ val title = it.selectFirst("> div.gridtitlek").text()
+ val poster = fixUrl(it.selectFirst("> img.grid__img").attr("src"))
+ AnimeSearchResponse(
+ title,
+ if (trimEpisode) href.removeRange(href.lastIndexOf('/'), href.length) else href,
+ this.name,
+ TvType.Anime,
+ poster,
+ null,
+ null,
+ EnumSet.of(DubStatus.Dubbed),
+ null,
+ null
+ )
+ }
+ }
+
+ override fun getMainPage(): HomePageResponse {
+ val trendingUrl = "$mainUrl/xz/trending.php?_=$unixTimeMS"
+ val lastEpisodeUrl = "$mainUrl/xz/epgrid.php?p=1&_=$unixTimeMS"
+ val recentlyAddedUrl = "$mainUrl/xz/gridgrabrecent.php?p=1&_=$unixTimeMS"
+ //val allUrl = "$mainUrl/xz/gridgrab.php?p=1&limit=12&_=$unixTimeMS"
+
+ val listItems = listOf(
+ HomePageList("Trending", parseDocumentTrending(trendingUrl)),
+ HomePageList("Recently Added", parseDocument(recentlyAddedUrl)),
+ HomePageList("Recent Releases", parseDocument(lastEpisodeUrl, true)),
+ // HomePageList("All", parseDocument(allUrl))
+ )
+
+ return HomePageResponse(listItems)
+ }
+
+
private fun getAnimeEpisode(slug: String, isMovie: Boolean): EpisodeInfo {
val url =
mainUrl + (if (isMovie) "/movies/jsonMovie" else "/xz/v3/jsonEpi") + ".php?slug=$slug&_=$unixTime"
@@ -76,8 +140,8 @@ class DubbedAnimeProvider : MainAPI() {
return href.replace("$mainUrl/", "")
}
- override fun quickSearch(query: String): ArrayList {
- val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=${unixTime}"
+ override fun quickSearch(query: String): List {
+ val url = "$mainUrl/xz/searchgrid.php?p=1&limit=12&s=$query&_=$unixTime"
val response = khttp.get(url)
val document = Jsoup.parse(response.text)
val items = document.select("div.grid__item > a")
@@ -111,7 +175,7 @@ class DubbedAnimeProvider : MainAPI() {
return returnValue
}
- override fun search(query: String): ArrayList {
+ override fun search(query: String): List {
val url = "$mainUrl/search/$query"
val response = khttp.get(url)
val document = Jsoup.parse(response.text)
@@ -188,9 +252,8 @@ class DubbedAnimeProvider : MainAPI() {
}
override fun load(url: String): LoadResponse {
- val slug = url.replace("$mainUrl/","")
- if (getIsMovie(slug)) {
- val realSlug = slug.replace("movies/", "")
+ if (getIsMovie(url)) {
+ val realSlug = url.replace("movies/", "")
val episode = getAnimeEpisode(realSlug, true)
val poster = episode.previewImg ?: episode.wideImg
return MovieLoadResponse(
@@ -205,7 +268,7 @@ class DubbedAnimeProvider : MainAPI() {
null
)
} else {
- val response = khttp.get("$mainUrl/$slug")
+ val response = khttp.get(url)
val document = Jsoup.parse(response.text)
val title = document.selectFirst("h4").text()
val descriptHeader = document.selectFirst("div.animeDescript")
@@ -220,7 +283,18 @@ class DubbedAnimeProvider : MainAPI() {
val img = fixUrl(document.select("div.fkimgs > img").attr("src"))
return AnimeLoadResponse(
- null, null, title, "$mainUrl/$slug", this.name, TvType.Anime, img, year, ArrayList(episodes), null, null, descript,
+ null,
+ null,
+ title,
+ url,
+ this.name,
+ TvType.Anime,
+ img,
+ year,
+ ArrayList(episodes),
+ null,
+ null,
+ descript,
)
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
index 7b9cba45..da39a603 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt
@@ -18,8 +18,7 @@ class APIRepository(val api: MainAPI) {
suspend fun load(url: String): Resource {
return safeApiCall {
- // remove suffix for some slugs to handle correctly
- api.load(url.removeSuffix("/")) ?: throw ErrorLoadingException()
+ api.load(api.fixUrl(url)) ?: throw ErrorLoadingException()
}
}
@@ -30,7 +29,7 @@ class APIRepository(val api: MainAPI) {
}
}
- suspend fun quickSearch(query: String): Resource> {
+ suspend fun quickSearch(query: String): Resource> {
return safeApiCall {
api.quickSearch(query) ?: throw ErrorLoadingException()
}
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 5f5e8b05..78d3234d 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
@@ -122,7 +122,12 @@ class HomeFragment : Fragment() {
val MAX_BREAK_COUNT = 10
while (random?.posterUrl == null) {
- random = home.items.random().list.random()
+ try {
+ random = home.items.random().list.random()
+ } catch (e : Exception) {
+ // probs Collection is empty.
+ }
+
breakCount++
if (breakCount > MAX_BREAK_COUNT) {
break
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
index f65fe939..f8dcee90 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
@@ -1,16 +1,30 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
+import android.widget.Toast
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
+import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
+import kotlin.concurrent.thread
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings, rootKey)
+ val updatePrefrence = findPreference(getString(R.string.manual_check_update_key))!!
+ updatePrefrence.setOnPreferenceClickListener {
+ thread {
+ if (!requireActivity().runAutoUpdate(false)) {
+ activity?.runOnUiThread {
+ Toast.makeText(this.context, "No Update Found", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ return@setOnPreferenceClickListener true
+ }
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
new file mode 100644
index 00000000..cc70366b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
@@ -0,0 +1,283 @@
+package com.lagradost.cloudstream3.utils
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.FileProvider
+import androidx.preference.PreferenceManager
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.lagradost.cloudstream3.BuildConfig
+import com.lagradost.cloudstream3.R
+import java.io.*
+import java.net.URL
+import java.net.URLConnection
+import kotlin.concurrent.thread
+
+const val UPDATE_TIME = 1000
+
+class InAppUpdater {
+ companion object {
+ // === IN APP UPDATER ===
+ data class GithubAsset(
+ @JsonProperty("name") val name: String,
+ @JsonProperty("size") val size: Int, // Size bytes
+ @JsonProperty("browser_download_url") val browser_download_url: String, // download link
+ @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive
+ )
+
+ data class GithubRelease(
+ @JsonProperty("tag_name") val tag_name: String, // Version code
+ @JsonProperty("body") val body: String, // Desc
+ @JsonProperty("assets") val assets: List,
+ @JsonProperty("target_commitish") val target_commitish: String, // branch
+ )
+
+ data class Update(
+ @JsonProperty("shouldUpdate") val shouldUpdate: Boolean,
+ @JsonProperty("updateURL") val updateURL: String?,
+ @JsonProperty("updateVersion") val updateVersion: String?,
+ @JsonProperty("changelog") val changelog: String?,
+ )
+
+ private val mapper = JsonMapper.builder().addModule(KotlinModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
+
+ private fun Activity.getAppUpdate(): Update {
+ try {
+ val url = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
+ val headers = mapOf("Accept" to "application/vnd.github.v3+json")
+ val response =
+ mapper.readValue>(khttp.get(url, headers = headers).text)
+
+ val versionRegex = Regex("""(.*?((\d)\.(\d)\.(\d)).*\.apk)""")
+
+ /*
+ val releases = response.map { it.assets }.flatten()
+ .filter { it.content_type == "application/vnd.android.package-archive" }
+ val found =
+ releases.sortedWith(compareBy {
+ versionRegex.find(it.name)?.groupValues?.get(2)
+ }).toList().lastOrNull()*/
+ val found =
+ response.sortedWith(compareBy { release ->
+ release.assets.filter { it.content_type == "application/vnd.android.package-archive" }
+ .getOrNull(0)?.name?.let { it1 ->
+ versionRegex.find(
+ it1
+ )?.groupValues?.get(2)
+ }
+ }).toList().lastOrNull()
+ val foundAsset = found?.assets?.getOrNull(0)
+ val currentVersion = packageName?.let {
+ packageManager.getPackageInfo(it,
+ 0)
+ }
+
+ val foundVersion = foundAsset?.name?.let { versionRegex.find(it) }
+ val shouldUpdate = if (found != null && foundAsset?.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.compareTo(
+ foundVersion.groupValues[2]
+ )!! < 0 else false
+ return if (foundVersion != null) {
+ Update(shouldUpdate, foundAsset.browser_download_url, foundVersion.groupValues[2], found.body)
+ } else {
+ Update(false, null, null, null)
+ }
+
+ } catch (e: Exception) {
+ println(e)
+ return Update(false, null, null, null)
+ }
+ }
+
+ private fun Activity.downloadUpdate(url: String): Boolean {
+ println("DOWNLOAD UPDATE $url")
+ var fullResume = false // IF FULL RESUME
+ try {
+ // =================== DOWNLOAD POSTERS AND SETUP PATH ===================
+ val path = filesDir.toString() +
+ "/Download/apk/update.apk"
+
+ // =================== MAKE DIRS ===================
+ val rFile = File(path)
+ try {
+ rFile.parentFile?.mkdirs()
+ } catch (_ex: Exception) {
+ println("FAILED:::$_ex")
+ }
+ val url = url.replace(" ", "%20")
+
+ val _url = URL(url)
+
+ val connection: URLConnection = _url.openConnection()
+
+ var bytesRead = 0L
+
+ // =================== STORAGE ===================
+ try {
+ if (!rFile.exists()) {
+ rFile.createNewFile()
+ } else {
+ rFile.delete()
+ rFile.createNewFile()
+ }
+ } catch (e: Exception) {
+ println(e)
+ runOnUiThread {
+ Toast.makeText(this, "Permission error", Toast.LENGTH_SHORT).show()
+ }
+ return false
+ }
+
+ // =================== CONNECTION ===================
+ connection.setRequestProperty("Accept-Encoding", "identity")
+ connection.connectTimeout = 10000
+ var clen = 0
+ try {
+ connection.connect()
+ clen = connection.contentLength
+ println("CONTENTN LENGTH: $clen")
+ } catch (_ex: Exception) {
+ println("CONNECT:::$_ex")
+ _ex.printStackTrace()
+ }
+
+ // =================== VALIDATE ===================
+ if (clen < 5000000) { // min of 5 MB
+ clen = 0
+ }
+ if (clen <= 0) { // TO SMALL OR INVALID
+ //showNot(0, 0, 0, DownloadType.IsFailed, info)
+ return false
+ }
+
+ // =================== SETUP VARIABLES ===================
+ //val bytesTotal: Long = (clen + bytesRead.toInt()).toLong()
+ val input: InputStream = BufferedInputStream(connection.inputStream)
+ val output: OutputStream = FileOutputStream(rFile, false)
+ var bytesPerSec = 0L
+ val buffer = ByteArray(1024)
+ var count: Int
+ //var lastUpdate = System.currentTimeMillis()
+
+ while (true) {
+ try {
+ count = input.read(buffer)
+ if (count < 0) break
+
+ bytesRead += count
+ bytesPerSec += count
+ output.write(buffer, 0, count)
+ } catch (_ex: Exception) {
+ println("CONNECT TRUE:::$_ex")
+ _ex.printStackTrace()
+ fullResume = true
+ break
+ }
+ }
+
+ if (fullResume) { // IF FULL RESUME DELETE CURRENT AND DONT SHOW DONE
+ with(NotificationManagerCompat.from(this)) {
+ cancel(-1)
+ }
+ }
+
+ output.flush()
+ output.close()
+ input.close()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val contentUri = FileProvider.getUriForFile(
+ this,
+ BuildConfig.APPLICATION_ID + ".provider",
+ rFile
+ )
+ val install = Intent(Intent.ACTION_VIEW)
+ install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ install.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ install.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
+ install.data = contentUri
+ startActivity(install)
+ return true
+ } else {
+ val apkUri = Uri.fromFile(rFile)
+ val install = Intent(Intent.ACTION_VIEW)
+ install.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ install.setDataAndType(
+ apkUri,
+ "application/vnd.android.package-archive"
+ )
+ startActivity(install)
+ return true
+ }
+
+ } catch (_ex: Exception) {
+ println("FATAL EX DOWNLOADING:::$_ex")
+ return false
+ }
+ }
+
+ fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+
+ if (!checkAutoUpdate || settingsManager.getBoolean(getString(R.string.auto_update_key), true)
+ ) {
+ val update = getAppUpdate()
+ if (update.shouldUpdate && update.updateURL != null) {
+ runOnUiThread {
+ val currentVersion = packageName?.let {
+ packageManager.getPackageInfo(it,
+ 0)
+ }
+
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ builder.setTitle("New update found!\n${currentVersion?.versionName} -> ${update.updateVersion}")
+ builder.setMessage("${update.changelog}")
+
+ val context = this
+ builder.apply {
+ setPositiveButton("Update") { _, _ ->
+ Toast.makeText(context, "Download started", Toast.LENGTH_LONG).show()
+ thread {
+ val downloadStatus = context.downloadUpdate(update.updateURL)
+ if (!downloadStatus) {
+ runOnUiThread {
+ Toast.makeText(context,
+ "Download Failed",
+ Toast.LENGTH_LONG).show()
+ }
+ } /*else {
+ activity.runOnUiThread {
+ Toast.makeText(localContext,
+ "Downloaded APK",
+ Toast.LENGTH_LONG).show()
+ }
+ }*/
+ }
+ }
+
+ setNegativeButton("Cancel") { _, _ -> }
+
+ if(checkAutoUpdate) {
+ setNeutralButton("Don't show again") { _, _ ->
+ settingsManager.edit().putBoolean("auto_update", false).apply()
+ }
+ }
+ }
+ builder.show()
+ }
+ return true
+ }
+ return false
+ }
+ return false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml
new file mode 100644
index 00000000..053e07ae
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_system_update_24.xml b/app/src/main/res/drawable/ic_baseline_system_update_24.xml
new file mode 100644
index 00000000..7ad99c9a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_system_update_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 3651b2c1..2d4a3a61 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -87,6 +87,7 @@
android:id="@+id/home_main_poster"
tools:src="@drawable/example_poster"
android:layout_gravity="center"
+ android:scaleType="centerCrop"
android:layout_width="150dp"
android:layout_height="212dp"
android:contentDescription="@string/home_main_poster">
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 97a60fe8..781eb895 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -83,4 +83,6 @@
Font
Search using providers
Search using types
+ auto_update
+ manual_check_update
\ No newline at end of file
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index d4273244..1dd14dbd 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -84,6 +84,18 @@
android:summaryOff="Only sends data on crashes"
android:summaryOn="Sends no data"
android:defaultValue="false"/>
+
+