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
105 changes: 80 additions & 25 deletions app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import java.net.URI
import java.security.SecureRandom

data class AuthLoginPage(
/** The website to open to authenticate */
/**
* The website to open to authenticate
*/
val url: String,
/**
* State/control code to verify against the redirectUrl to make sure the request is valid.
Expand All @@ -33,13 +35,19 @@ data class AuthToken(
*/
@JsonProperty("accessToken") @SerialName("accessToken")
val accessToken: String? = null,
/** For OAuth a special refresh token is issues to refresh the access token. */
/**
* For OAuth a special refresh token is issues to refresh the access token.
*/
@JsonProperty("refreshToken") @SerialName("refreshToken")
val refreshToken: String? = null,
/** In UnixTime (sec) when it expires */
/**
* In UnixTime (sec) when it expires
*/
@JsonProperty("accessTokenLifetime") @SerialName("accessTokenLifetime")
val accessTokenLifetime: Long? = null,
/** In UnixTime (sec) when it expires */
/**
* In UnixTime (sec) when it expires
* */
@JsonProperty("refreshTokenLifetime") @SerialName("refreshTokenLifetime")
val refreshTokenLifetime: Long? = null,
/**
Expand All @@ -59,7 +67,9 @@ data class AuthToken(
@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now
@Serializable
data class AuthUser(
/** Account display-name, can also be email if name does not exist */
/**
* Account display-name, can also be email if name does not exist
*/
@JsonProperty("name") @SerialName("name")
val name: String?,
/**
Expand All @@ -68,10 +78,14 @@ data class AuthUser(
*/
@JsonProperty("id") @SerialName("id")
val id: Int,
/** Profile picture URL */
/**
* Profile picture URL
*/
@JsonProperty("profilePicture") @SerialName("profilePicture")
val profilePicture: String? = null,
/** Profile picture Headers of the URL */
/**
* Profile picture Headers of the URL
*/
@JsonProperty("profilePictureHeaders") @JsonAlias("profilePictureHeader")
@SerialName("profilePictureHeaders") @JsonNames("profilePictureHeader")
val profilePictureHeaders: Map<String, String>? = null,
Expand Down Expand Up @@ -102,7 +116,9 @@ data class AuthPinData(
val interval: Int,
)

/** The login field requirements to display to the user */
/**
* The login field requirements to display to the user
*/
data class AuthLoginRequirement(
val password: Boolean = false,
val username: Boolean = false,
Expand All @@ -119,35 +135,58 @@ data class AuthLoginResponse(
@JsonProperty("server") @SerialName("server") val server: String?,
)

/** Stateless Authentication class used for all personalized content */
/**
* Stateless Authentication class used for all personalized content
*/
abstract class AuthAPI {
open val name: String = "NONE"
open val idPrefix: String = "NONE"

/** Drawable icon of the service */
/**
* Drawable icon of the service
*/
open val icon: Int? = null

/** If this service requires an account to use */
/**
* If this service requires an account to use
* */
open val requiresLogin: Boolean = true

/** Link to a website for creating a new account */
/**
* Link to a website for creating a new account
* */
open val createAccountUrl: String? = null

/** The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login */
/**
* The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login
*/
open val redirectUrlIdentifier: String? = null

/** Has OAuth2 login support, including login, loginRequest and refreshToken */
/**
* Has OAuth2 login support, including login, loginRequest and refreshToken
*/
open val hasOAuth2: Boolean = false

/** Has on device pin support, aka login with a QR code */
/**
* Has on device pin support, aka login with a QR code
*/
open val hasPin: Boolean = false

/** Has in app login support, aka login with a dialog */
/**
* Has in app login support, aka login with a dialog
*/
open val hasInApp: Boolean = false

/** The requirements to login in app */
/**
* The requirements to login in app
*/
open val inAppLoginRequirement: AuthLoginRequirement? = null

/** Determine if SyncApi supports Playback Scrobbling.
Comment thread
KingLucius marked this conversation as resolved.
* @see SyncAPI.onPlaybackStatus
*/
open val supportScrobble: Boolean = false

companion object {
@Deprecated(
message = "Use APIHolder.unixTime instead",
Expand Down Expand Up @@ -190,37 +229,53 @@ abstract class AuthAPI {
}
}

/** Is this url a valid redirect url for this service? */
/**
* Is this url a valid redirect url for this service?
*/
@Throws
open fun isValidRedirectUrl(url: String): Boolean =
redirectUrlIdentifier != null && url.contains("/$redirectUrlIdentifier")

/** OAuth2 login from a valid redirectUrl, and payload given in loginRequest */
/**
* OAuth2 login from a valid redirectUrl, and payload given in loginRequest
*/
@Throws
open suspend fun login(redirectUrl: String, payload: String?): AuthToken? =
throw NotImplementedError()

/** OAuth2 login request, asking the service to provide a url to open in the browser */
/**
* OAuth2 login request, asking the service to provide a url to open in the browser
*/
@Throws
open fun loginRequest(): AuthLoginPage? = throw NotImplementedError()

/** Pin login request, asking the service to provide an verificationUrl to display with a QR code */
/**
* Pin login request, asking the service to provide an verificationUrl to display with a QR code
* */
@Throws
open suspend fun pinRequest(): AuthPinData? = throw NotImplementedError()

/** OAuth2 token refresh, this ensures that all token passed to other functions will be valid */
/**
* OAuth2 token refresh, this ensures that all token passed to other functions will be valid
*/
@Throws
open suspend fun refreshToken(token: AuthToken): AuthToken? = throw NotImplementedError()

/** Pin login, this will be called periodically while logging in to check if the pin has been verified by the user */
/**
* Pin login, this will be called periodically while logging in to check if the pin has been verified by the user
*/
@Throws
open suspend fun login(payload: AuthPinData): AuthToken? = throw NotImplementedError()

/** In app login */
/**
* In-app login
*/
@Throws
open suspend fun login(form: AuthLoginResponse): AuthToken? = throw NotImplementedError()

/** Get the visible user account */
/**
* Get the visible user account
*/
@Throws
open suspend fun user(token: AuthToken?): AuthUser? = throw NotImplementedError()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
import com.lagradost.cloudstream3.utils.PreferenceDelegate
import com.lagradost.cloudstream3.utils.txt

/** General-purpose repo */
/**
* General-purpose repo
*/
class PlainAuthRepo(api: AuthAPI) : AuthRepo(api)

/** Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management. */
/**
* Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management.
*/
abstract class AuthRepo(open val api: AuthAPI) {
fun isValidRedirectUrl(url: String) = safe { api.isValidRedirectUrl(url) } ?: false
val idPrefix get() = api.idPrefix
Expand All @@ -25,6 +30,15 @@ abstract class AuthRepo(open val api: AuthAPI) {
val hasInApp get() = api.hasInApp
val inAppLoginRequirement get() = api.inAppLoginRequirement
val isAvailable get() = !api.requiresLogin || authUser() != null
val supportScrobble get() = api.supportScrobble

private val supportScrobbleDelegate: PreferenceDelegate<Boolean> by lazy {
PreferenceDelegate("$idPrefix/supportScrobble", api.supportScrobble)
}

var scrobbleEnabled: Boolean
get() = supportScrobbleDelegate.getValue(this, ::scrobbleEnabled)
set(value) = supportScrobbleDelegate.setValue(this, ::scrobbleEnabled, value)

companion object {
private val oauthPayload: MutableMap<String, String?> = mutableMapOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ abstract class SyncAPI : AuthAPI() {
open var requireLibraryRefresh: Boolean = true
open val mainUrl: String = "NONE"

/** Currently unused, but will be used to correctly render the UI.
* This should specify what sync watch types can be used with this service. */
/**
* Currently unused, but will be used to correctly render the UI.
* This should specify what sync watch types can be used with this service.
* */
open val supportedWatchTypes: Set<SyncWatchType> = SyncWatchType.entries.toSet()

/**
* Allows certain providers to open pages from
* library links.
**/
open val syncIdName: SyncIdName? = null

/** Modify the current status of an item */
/**
* Modify the current status of an item
* */
@Throws
@WorkerThread
open suspend fun updateStatus(
Expand All @@ -44,32 +49,81 @@ abstract class SyncAPI : AuthAPI() {
newStatus: AbstractSyncStatus
): Boolean = throw NotImplementedError()

/** Get the current status of an item */
/**
* Get the current status of an item
* */
@Throws
@WorkerThread
open suspend fun status(auth: AuthData?, id: String): AbstractSyncStatus? =
throw NotImplementedError()

/** Get metadata about an item */
/**
* Get metadata about an item
* */
@Throws
@WorkerThread
open suspend fun load(auth: AuthData?, id: String): SyncResult? = throw NotImplementedError()

/** Search this service for any results for a given query */
/**
* Search this service for any results for a given query
* */
@Throws
@WorkerThread
open suspend fun search(auth: AuthData?, query: String): List<SyncSearchResult>? =
throw NotImplementedError()

/** Get the current library/bookmarks of this service */
/**
* Get the current library/bookmarks of this service
* */
@Throws
@WorkerThread
open suspend fun library(auth: AuthData?): LibraryMetadata? = throw NotImplementedError()

/** Helper function, may be used in the future */
/**
* Helper function, may be used in the future
* */
@Throws
open fun urlToId(url: String): String? = null

@Throws
@WorkerThread
open suspend fun onPlaybackStatus(
auth: AuthData?,
progress: PlaybackProgress,
status: PlaybackStatus
): Boolean = false

enum class PlaybackStatus {
Started,
Paused,
Stopped
}

/**
* Triggers on every player event (Start/Pause/Stop/Ended)
* */
data class PlaybackProgress(
/**
* Show/Movie Sync level ID
* */
val id: String,
/**
* Season Number, Should be null for Movies
* */
val season: Int?,
/**
* Episode Number, Should be null for Movies
* */
val episode: Int?,
val type: TvType?,
val positionMs: Long,
val durationMs: Long,
) {
val progressPercentage: Double
get() = if (durationMs <= 0L) 0.0 else
(positionMs.toDouble() / durationMs.toDouble() * 100.0).coerceIn(0.0, 100.0)
}

data class SyncSearchResult(
override val name: String,
override val apiName: String,
Expand Down Expand Up @@ -120,9 +174,13 @@ abstract class SyncAPI : AuthAPI() {
var posterUrl: String? = null,
var backgroundPosterUrl: String? = null,

/** In unixtime */
/**
* In unixtime
* */
var startDate: Long? = null,
/** In unixtime */
/**
* In unixtime
* */
var endDate: Long? = null,
var recommendations: List<SyncSearchResult>? = null,
var nextSeason: SyncSearchResult? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.syncproviders

/** Stateless safe abstraction of SyncAPI */
/**
* Stateless safe abstraction of SyncAPI
* */
class SyncRepo(override val api: SyncAPI) : AuthRepo(api) {
val syncIdName = api.syncIdName
var requireLibraryRefresh: Boolean
Expand All @@ -27,4 +29,16 @@ class SyncRepo(override val api: SyncAPI) : AuthRepo(api) {
suspend fun library(): Result<SyncAPI.LibraryMetadata?> = runCatching {
api.library(freshAuth())
}

suspend fun onPlaybackStatus(
progress: SyncAPI.PlaybackProgress,
status: SyncAPI.PlaybackStatus
): Result<Boolean> =
runCatching {
api.onPlaybackStatus(
freshAuth(),
progress,
status
)
}
}
Loading