mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Add SubScene (#923)
* Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent * Add SubScene and change subtitle API Co-authored-by: Aymanbest <51868001+aymanbest@users.noreply.github.com> * Fix file deletion --------- Co-authored-by: Aymanbest <51868001+aymanbest@users.noreply.github.com>
This commit is contained in:
parent
9ea7674a0f
commit
2b7d102716
9 changed files with 333 additions and 65 deletions
|
@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||
|
@ -290,6 +291,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
const val ANIMATED_OUTLINE: Boolean = false
|
||||
var lastError: String? = null
|
||||
|
||||
private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
|
||||
|
||||
/**
|
||||
* Transient files to delete on application exit.
|
||||
* Deletes files on onDestroy().
|
||||
*/
|
||||
private var filesToDelete: Set<String>
|
||||
// This needs to be persistent because the application may exit without calling onDestroy.
|
||||
get() = getKey<Set<String>>(FILE_DELETE_KEY) ?: setOf()
|
||||
private set(value) = setKey(FILE_DELETE_KEY, value)
|
||||
|
||||
/**
|
||||
* Add file to delete on Exit.
|
||||
*/
|
||||
fun deleteFileOnExit(file: File) {
|
||||
filesToDelete = filesToDelete + file.path
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting this will automatically enter the query in the search
|
||||
* next time the search fragment is opened.
|
||||
|
@ -676,6 +695,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
filesToDelete.forEach { path ->
|
||||
val result = File(path).deleteRecursively()
|
||||
if (result) {
|
||||
Log.d(TAG, "Deleted temporary file: $path")
|
||||
} else {
|
||||
Log.d(TAG, "Failed to delete temporary file: $path")
|
||||
}
|
||||
}
|
||||
filesToDelete = setOf()
|
||||
val broadcastIntent = Intent()
|
||||
broadcastIntent.action = "restart_service"
|
||||
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
|
||||
|
@ -1654,7 +1682,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
// this ensures that no unnecessary space is taken
|
||||
loadCache()
|
||||
File(filesDir, "exoplayer").deleteRecursively() // old cache
|
||||
File(cacheDir, "exoplayer").deleteOnExit() // current cache
|
||||
deleteFileOnExit(File(cacheDir, "exoplayer")) // current cache
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
package com.lagradost.cloudstream3.subtitles
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import java.io.File
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
interface AbstractSubProvider {
|
||||
val idPrefix: String
|
||||
|
||||
@WorkerThread
|
||||
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
|
||||
throw NotImplementedError()
|
||||
|
@ -15,6 +27,98 @@ interface AbstractSubProvider {
|
|||
suspend fun load(data: SubtitleEntity): String? {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
|
||||
this.addUrl(load(data))
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun getResource(data: SubtitleEntity): SubtitleResource {
|
||||
return SubtitleResource().apply {
|
||||
this.getResources(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for subtitle files.
|
||||
* @see addUrl
|
||||
* @see addFile
|
||||
*/
|
||||
class SubtitleResource {
|
||||
fun downloadFile(source: BufferedSource): File {
|
||||
val file = File.createTempFile("temp-subtitle", ".tmp").apply {
|
||||
deleteFileOnExit(this)
|
||||
}
|
||||
val sink = file.sink().buffer()
|
||||
sink.writeAll(source)
|
||||
sink.close()
|
||||
source.close()
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun unzip(file: File): List<Pair<String, File>> {
|
||||
val entries = mutableListOf<Pair<String, File>>()
|
||||
|
||||
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
||||
var zipEntry = zipInputStream.nextEntry
|
||||
|
||||
while (zipEntry != null) {
|
||||
val tempFile = File.createTempFile("unzipped-subtitle", ".tmp").apply {
|
||||
deleteFileOnExit(this)
|
||||
}
|
||||
entries.add(zipEntry.name to tempFile)
|
||||
|
||||
tempFile.sink().buffer().use { buffer ->
|
||||
buffer.writeAll(zipInputStream.source())
|
||||
}
|
||||
|
||||
zipEntry = zipInputStream.nextEntry
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
data class SingleSubtitleResource(
|
||||
val name: String?,
|
||||
val url: String,
|
||||
val origin: SubtitleOrigin
|
||||
)
|
||||
|
||||
private var resources: MutableList<SingleSubtitleResource> = mutableListOf()
|
||||
|
||||
fun getSubtitles(): List<SingleSubtitleResource> {
|
||||
return resources.toList()
|
||||
}
|
||||
|
||||
fun addUrl(url: String?, name: String? = null) {
|
||||
if (url == null) return
|
||||
this.resources.add(
|
||||
SingleSubtitleResource(name, url, SubtitleOrigin.URL)
|
||||
)
|
||||
}
|
||||
|
||||
fun addFile(file: File, name: String? = null) {
|
||||
this.resources.add(
|
||||
SingleSubtitleResource(name, file.toUri().toString(), SubtitleOrigin.DOWNLOADED_FILE)
|
||||
)
|
||||
deleteFileOnExit(file)
|
||||
}
|
||||
|
||||
suspend fun addZipUrl(
|
||||
url: String,
|
||||
nameGenerator: (String, File) -> String? = { _, _ -> null }
|
||||
) {
|
||||
val source = app.get(url).okhttpResponse.body.source()
|
||||
val zip = downloadFile(source)
|
||||
val realFiles = unzip(zip)
|
||||
zip.deleteRecursively()
|
||||
realFiles.forEach { (name, subtitleFile) ->
|
||||
addFile(subtitleFile, nameGenerator(name, subtitleFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AbstractSubApi : AbstractSubProvider, AuthAPI
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.syncproviders
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -14,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
val simklApi = SimklApi(0)
|
||||
val indexSubtitlesApi = IndexSubtitleApi()
|
||||
val addic7ed = Addic7ed()
|
||||
val subScene = SubScene()
|
||||
val localListApi = LocalList()
|
||||
|
||||
// used to login via app intent
|
||||
|
@ -41,7 +43,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
get() = listOf(
|
||||
openSubtitlesApi,
|
||||
indexSubtitlesApi, // they got anti scraping measures in place :(
|
||||
addic7ed
|
||||
addic7ed,
|
||||
subScene
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
|
|
@ -23,28 +23,8 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
companion object {
|
||||
const val host = "https://indexsubtitle.com"
|
||||
const val TAG = "INDEXSUBS"
|
||||
}
|
||||
|
||||
private fun fixUrl(url: String): String {
|
||||
if (url.startsWith("http")) {
|
||||
return url
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val startsWithNoHttp = url.startsWith("//")
|
||||
if (startsWithNoHttp) {
|
||||
return "https:$url"
|
||||
} else {
|
||||
if (url.startsWith('/')) {
|
||||
return host + url
|
||||
}
|
||||
return "$host/$url"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrdinal(num: Int?): String? {
|
||||
fun getOrdinal(num: Int?): String? {
|
||||
return when (num) {
|
||||
1 -> "First"
|
||||
2 -> "Second"
|
||||
|
@ -84,6 +64,26 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixUrl(url: String): String {
|
||||
if (url.startsWith("http")) {
|
||||
return url
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val startsWithNoHttp = url.startsWith("//")
|
||||
if (startsWithNoHttp) {
|
||||
return "https:$url"
|
||||
} else {
|
||||
if (url.startsWith('/')) {
|
||||
return host + url
|
||||
}
|
||||
return "$host/$url"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
||||
val FILTER_EPS_REGEX =
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
|
||||
class SubScene : AbstractSubProvider {
|
||||
val mainUrl = "https://subscene.com"
|
||||
val name = "Subscene"
|
||||
override val idPrefix = "subscene"
|
||||
|
||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||
val seasonName =
|
||||
query.seasonNumber?.let { number ->
|
||||
// Need to translate "7" to "Seventh Season"
|
||||
getOrdinal(number)?.let { words -> " - $words Season" }
|
||||
} ?: ""
|
||||
|
||||
val fullQuery = query.query + seasonName
|
||||
|
||||
val doc = app.post(
|
||||
"$mainUrl/subtitles/searchbytitle",
|
||||
data = mapOf("query" to fullQuery, "l" to "")
|
||||
).document
|
||||
|
||||
return doc.select("div.title a").map { element ->
|
||||
val href = "$mainUrl${element.attr("href")}"
|
||||
val title = element.text()
|
||||
|
||||
AbstractSubtitleEntities.SubtitleEntity(
|
||||
idPrefix = idPrefix,
|
||||
name = title,
|
||||
source = name,
|
||||
data = href,
|
||||
lang = query.lang ?: "en",
|
||||
epNumber = query.epNumber
|
||||
)
|
||||
}.distinctBy { it.data }
|
||||
}
|
||||
|
||||
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||
val resultDoc = app.get(data.data).document
|
||||
val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English"
|
||||
|
||||
val results = resultDoc.select("table tbody tr").mapNotNull { element ->
|
||||
val anchor = element.select("a")
|
||||
val href = anchor.attr("href") ?: return@mapNotNull null
|
||||
val fixedHref = "$mainUrl${href}"
|
||||
val spans = anchor.select("span")
|
||||
val language = spans.firstOrNull()?.text()
|
||||
val title = spans.getOrNull(1)?.text()
|
||||
val isPositive = anchor.select("span.positive-icon").isNotEmpty()
|
||||
|
||||
TableElement(title, language, fixedHref, isPositive)
|
||||
}.sortedBy {
|
||||
it.getScore(queryLanguage, data.epNumber)
|
||||
}
|
||||
|
||||
debugPrint { "$name found subtitles: ${results.takeLast(3)}" }
|
||||
// Last = highest score
|
||||
val selectedResult = results.lastOrNull() ?: return
|
||||
|
||||
val subtitleDocument = app.get(selectedResult.href).document
|
||||
val subtitleDownloadUrl =
|
||||
"$mainUrl${subtitleDocument.select("div.download a").attr("href")}"
|
||||
|
||||
this.addZipUrl(subtitleDownloadUrl) { name, _ ->
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to manage the various different subtitle results and rank them.
|
||||
*/
|
||||
data class TableElement(
|
||||
val title: String?,
|
||||
val language: String?,
|
||||
val href: String,
|
||||
val isPositive: Boolean
|
||||
) {
|
||||
private fun matchesLanguage(other: String): Boolean {
|
||||
return language != null && (language.contains(other, ignoreCase = true) ||
|
||||
other.contains(language, ignoreCase = true))
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores in this order:
|
||||
* Preferred Language > Episode number > Positive rating > English Language
|
||||
*/
|
||||
fun getScore(queryLanguage: String, episodeNum: Int?): Int {
|
||||
var score = 0
|
||||
if (this.matchesLanguage(queryLanguage)) {
|
||||
score += 8
|
||||
}
|
||||
// Matches Episode 7 using "E07" with any number of leading zeroes
|
||||
if (episodeNum != null && title != null && title.contains(
|
||||
Regex(
|
||||
"""E0*${episodeNum}""",
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
)
|
||||
) {
|
||||
score += 4
|
||||
}
|
||||
if (isPositive) {
|
||||
score += 2
|
||||
}
|
||||
if (this.matchesLanguage("English")) {
|
||||
score += 1
|
||||
}
|
||||
return score
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ import androidx.preference.PreferenceManager
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
|
@ -657,7 +658,7 @@ class CS3IPlayer : IPlayer {
|
|||
SimpleCache(
|
||||
File(
|
||||
context.cacheDir, "exoplayer"
|
||||
).also { it.deleteOnExit() }, // Ensures always fresh file
|
||||
).also { deleteFileOnExit(it) }, // Ensures always fresh file
|
||||
LeastRecentlyUsedCacheEvictor(cacheSize),
|
||||
databaseProvider
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
|||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||
|
@ -69,7 +70,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
val subsProviders
|
||||
get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null }
|
||||
get() = subtitleProviders.filter { provider ->
|
||||
(provider as? AbstractSubApi)?.let { !it.requiresLogin || it.loginInfo() != null }
|
||||
?: true
|
||||
}
|
||||
val subsProvidersIsActive
|
||||
get() = subsProviders.isNotEmpty()
|
||||
}
|
||||
|
@ -473,17 +477,21 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
currentSubtitle?.let { currentSubtitle ->
|
||||
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
|
||||
ioSafe {
|
||||
val url = api.load(currentSubtitle) ?: return@ioSafe
|
||||
val subtitle = SubtitleData(
|
||||
name = getName(currentSubtitle, true),
|
||||
url = url,
|
||||
origin = SubtitleOrigin.URL,
|
||||
mimeType = url.toSubtitleMimeType(),
|
||||
val subtitles =
|
||||
api.getResource(currentSubtitle).getSubtitles().map { resource ->
|
||||
SubtitleData(
|
||||
name = resource.name ?: getName(currentSubtitle, true),
|
||||
url = resource.url,
|
||||
origin = resource.origin,
|
||||
mimeType = resource.url.toSubtitleMimeType(),
|
||||
headers = currentSubtitle.headers,
|
||||
currentSubtitle.lang
|
||||
)
|
||||
}
|
||||
if (subtitles.isNotEmpty()) {
|
||||
runOnMainThread {
|
||||
addAndSelectSubtitles(subtitle)
|
||||
addAndSelectSubtitles(*subtitles.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -521,7 +529,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
|
||||
private fun addAndSelectSubtitles(
|
||||
vararg subtitleData: SubtitleData
|
||||
) {
|
||||
if (subtitleData.isEmpty()) return
|
||||
val selectedSubtitle = subtitleData.first()
|
||||
val ctx = context ?: return
|
||||
|
||||
val subs = currentSubs + subtitleData
|
||||
|
@ -533,13 +545,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
|
||||
setSubtitles(subtitleData)
|
||||
viewModel.addSubtitles(setOf(subtitleData))
|
||||
setSubtitles(selectedSubtitle)
|
||||
viewModel.addSubtitles(subtitleData.toSet())
|
||||
|
||||
selectSourceDialog?.dismissSafe()
|
||||
|
||||
showToast(
|
||||
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
|
||||
String.format(ctx.getString(R.string.player_loaded_subtitles), selectedSubtitle.name),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import okio.buffer
|
|||
import okio.sink
|
||||
import java.io.File
|
||||
import android.text.TextUtils
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
|
@ -213,7 +214,7 @@ class InAppUpdater {
|
|||
this.cacheDir.listFiles()?.filter {
|
||||
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
||||
}?.forEach {
|
||||
it.deleteOnExit()
|
||||
deleteFileOnExit(it)
|
||||
}
|
||||
|
||||
val downloadedFile = File.createTempFile(appUpdateName, ".$appUpdateSuffix")
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.os.IBinder
|
|||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||
|
@ -75,7 +76,7 @@ class PackageInstallerService : Service() {
|
|||
this@PackageInstallerService.cacheDir.listFiles()?.filter {
|
||||
it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix
|
||||
}?.forEach {
|
||||
it.deleteOnExit()
|
||||
deleteFileOnExit(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue