Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build.VERSION.SDK_INT
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import androidx.work.*
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
Expand Down Expand Up @@ -78,15 +80,6 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
.setSmallIcon(com.google.android.gms.cast.framework.R.drawable.quantum_ic_refresh_white_24)
.setProgress(0, 0, true)

private val updateNotificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)

private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

Expand Down Expand Up @@ -137,8 +130,8 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
val api = getApiFromNameNull(savedData.apiName) ?: return@amap null

// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
val loadResponse = withTimeoutOrNull(60_000) {
api.load(savedData.url)
} ?: return@amap null

val dubPreference =
Expand All @@ -150,8 +143,38 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
DubStatus.Subbed
}

var season = 0
val response = loadResponse as? EpisodeResponse ?: return@amap null
val latestEpisodes = response.getLatestEpisodes()
val latestPreferredEpisode = latestEpisodes[dubPreference]
val nextAiring = response.nextAiring

if (nextAiring != null && nextAiring.unixTime > unixTime) {
EpisodeAlertManager.scheduleEpisodeAlert(
subscribedData = savedData,
nextAiring = nextAiring,
episodeResponse = response,
apiName = api.name,
context = applicationContext,
)
updateProgress(max, ++progress, false)

// Early return to prevent notifying users of unavailable episodes
// on the rare occasion latestPreferredEpisode changes for meta providers
return@amap Unit
}

when (loadResponse) {
is TvSeriesLoadResponse -> {
season = loadResponse.episodes.maxOf { it.season ?: Int.MIN_VALUE }
}

is AnimeLoadResponse -> {
loadResponse.episodes[dubPreference]?.let { episodes ->
season = episodes.maxOf { it.season ?: Int.MIN_VALUE }
}
}
}

val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
val latestSeenEpisode =
Expand All @@ -173,38 +196,14 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
)

if (shouldUpdate) {
val updateHeader = savedData.name
val updateDescription = txt(
R.string.subscription_episode_released,
latestEpisode,
savedData.name
).asString(context)

val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}.putExtra(MainActivity.API_NAME_EXTRA_KEY, api.name)

val pendingIntent =
PendingIntentCompat.getActivity(context, 0, intent, 0, false)

val poster = ioWork {
savedData.posterUrl?.let { url ->
context.getImageBitmapFromUrl(
url,
savedData.posterHeaders
)
}
}

val updateNotification =
updateNotificationBuilder.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)
.setLargeIcon(poster)
.build()

notificationManager.notify(id, updateNotification)
EpisodeAlertManager.showEpisodeNotification(
id = id,
season = season,
episode = latestEpisode,
savedData = savedData,
apiName = api.name,
context = context
)
}

// You can probably get some issues here since this is async but it does not matter much.
Expand All @@ -223,4 +222,175 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
return Result.success()
}
}
}

class EpisodeAlertWorker(
val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
companion object {
const val SUBSCRIPTION_TAG = "SUBSCRIPTION_TAG"
const val SUBSCRIPTION_ID = "subscription_id"
const val EPISODE_NO = "episode_no"
const val SEASON_NO = "season_no"
const val API_NAME = "api_name"
}

override suspend fun doWork(): Result {

val subscriptionId = inputData.getInt(SUBSCRIPTION_ID, -1)
val episode = inputData.getInt(EPISODE_NO, -1)
val season = inputData.getInt(SEASON_NO, -1)
val apiName = inputData.getString(API_NAME) ?: return Result.success()

// Final check to ensure user is still subscribed before notifying
val savedData = DataStoreHelper.getSubscribedData(subscriptionId) ?: return Result.success()
val id = savedData.id ?: return Result.success()

EpisodeAlertManager.showEpisodeNotification(
id = id,
season = season,
episode = episode,
savedData = savedData,
apiName = apiName,
context = applicationContext
)

return Result.success()
}
}

class EpisodeAlertManager {
companion object {
fun scheduleEpisodeAlert(
subscribedData: DataStoreHelper.SubscribedData,
nextAiring: NextAiring?,
episodeResponse: EpisodeResponse?,
apiName: String,
context: Context
) {
val now = unixTime

when {
nextAiring == null -> {
DataStoreHelper.updateSubscribedData(
subscribedData.id,
subscribedData,
episodeResponse
)
}

nextAiring.unixTime > now && nextAiring.unixTime < now + 31_556_926L -> {
val episodeKey = "${nextAiring.season ?: ""}_${nextAiring.episode}"
val uniqueWorkName = "${apiName}_${subscribedData.id}_$episodeKey"
val delay = nextAiring.unixTime - now

// Work manager's replace policy will take care of rescheduling if the air time changes
enqueueEpisodeAlertWorker(
subscribedData = subscribedData,
nextAiring = nextAiring,
apiName = apiName,
uniqueWorkName = uniqueWorkName,
delay = delay,
context = context,
)

DataStoreHelper.updateSubscribedData(
subscribedData.id,
subscribedData,
episodeResponse
)
}
}
}

private fun enqueueEpisodeAlertWorker(
subscribedData: DataStoreHelper.SubscribedData,
nextAiring: NextAiring,
apiName: String,
uniqueWorkName: String,
delay: Long,
context: Context
) {
val inputData = workDataOf(
EpisodeAlertWorker.SUBSCRIPTION_ID to subscribedData.id,
EpisodeAlertWorker.EPISODE_NO to nextAiring.episode,
EpisodeAlertWorker.SEASON_NO to nextAiring.season,
EpisodeAlertWorker.API_NAME to apiName,
)

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request = OneTimeWorkRequestBuilder<EpisodeAlertWorker>()
.setInitialDelay(delay, TimeUnit.SECONDS)
.addTag(EpisodeAlertWorker.SUBSCRIPTION_TAG)
.setInputData(inputData)
.setConstraints(constraints)
.build()

WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName,
ExistingWorkPolicy.REPLACE,
request
)
}

suspend fun showEpisodeNotification(
id: Int,
season: Int,
episode: Int,
savedData: DataStoreHelper.SubscribedData,
apiName: String,
context: Context
) {
val updateHeader = savedData.name
val posterUrl = savedData.posterUrl
val posterHeaders = savedData.posterHeaders
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(MainActivity.API_NAME_EXTRA_KEY, apiName)
}

val pendingIntent =
PendingIntentCompat.getActivity(context, 0, intent, 0, false)

val updateDescription = if (season > 0) {
txt(
R.string.subscription_season_episode_released,
season,
episode,
).asString(context)
} else {
txt(
R.string.subscription_episode_released,
episode,
).asString(context)
}

val notificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)

val poster = ioWork {
posterUrl?.let { url ->
context.getImageBitmapFromUrl(url, posterHeaders)
}
}
notificationBuilder.setLargeIcon(poster)

NotificationManagerCompat.from(context)
.notify(id, notificationBuilder.build())
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@
<string name="subscription_new">Subscribed to %s</string>
<string name="subscription_deleted">Unsubscribed from %s</string>
<string name="subscription_episode_released">Episode %d released!</string>
<string name="subscription_season_episode_released">Season %d Episode %d released!</string>
<string name="action_subscribe">Subscribe</string>
<string name="action_unsubscribe">Unsubscribe</string>
<string name="profile_number">Profile %d</string>
Expand Down