Skip to content
Draft
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 @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi
import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi
import com.lagradost.cloudstream3.syncproviders.providers.TraktApi
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
Expand Down Expand Up @@ -276,7 +277,7 @@ 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.
/** 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()
/**
Expand Down Expand Up @@ -732,6 +733,7 @@ abstract class AccountManager {
val malApi = MALApi()
val aniListApi = AniListApi()
val simklApi = SimklApi()
val traktApi = TraktApi()
val localListApi = LocalList()

val openSubtitlesApi = OpenSubtitlesApi()
Expand Down Expand Up @@ -773,6 +775,7 @@ abstract class AccountManager {
SyncRepo(malApi),
SyncRepo(aniListApi),
SyncRepo(simklApi),
SyncRepo(traktApi),
SyncRepo(localListApi),

SubtitleRepo(openSubtitlesApi),
Expand Down Expand Up @@ -822,6 +825,7 @@ abstract class AccountManager {
LoadResponse.malIdPrefix = malApi.idPrefix
LoadResponse.aniListIdPrefix = aniListApi.idPrefix
LoadResponse.simklIdPrefix = simklApi.idPrefix
LoadResponse.traktIdPrefix = traktApi.idPrefix
}

val subtitleProviders = arrayOf(
Expand All @@ -834,6 +838,7 @@ abstract class AccountManager {
SyncRepo(malApi),
SyncRepo(aniListApi),
SyncRepo(simklApi),
SyncRepo(traktApi),
SyncRepo(localListApi)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package com.lagradost.cloudstream3.syncproviders.providers

import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.syncproviders.AuthLoginPage
import com.lagradost.cloudstream3.syncproviders.AuthToken
import com.lagradost.cloudstream3.syncproviders.AuthUser
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.SyncWatchType

/* https://trakt.docs.apiary.io */
class TraktApi : SyncAPI() {
override val name = "Trakt"
override val idPrefix = "trakt"

override val mainUrl = "https://trakt.tv"
val api = "https://api.trakt.tv"

override val supportedWatchTypes: Set<SyncWatchType> = emptySet()

override val icon = R.drawable.trakt
override val hasOAuth2 = true
override val redirectUrlIdentifier = "NONE"
val redirectUri = "cloudstreamapp://$redirectUrlIdentifier"

companion object {
val id: String get() = throw NotImplementedError()
val secret: String get() = throw NotImplementedError()

fun getHeaders(token: AuthToken) = mapOf(
"Authorization" to "Bearer ${token.accessToken}",
"Content-Type" to "application/json",
"trakt-api-version" to "2",
"trakt-api-key" to id,
)
}

data class TokenRoot(
@JsonProperty("access_token")
val accessToken: String,
@JsonProperty("token_type")
val tokenType: String,
@JsonProperty("expires_in")
val expiresIn: Long,
@JsonProperty("refresh_token")
val refreshToken: String,
@JsonProperty("scope")
val scope: String,
@JsonProperty("created_at")
val createdAt: Long,
)

data class UserRoot(
@JsonProperty("username")
val username: String,
@JsonProperty("private")
val private: Boolean?,
@JsonProperty("name")
val name: String,
@JsonProperty("vip")
val vip: Boolean?,
@JsonProperty("vip_ep")
val vipEp: Boolean?,
@JsonProperty("ids")
val ids: Ids?,
@JsonProperty("joined_at")
val joinedAt: String?,
@JsonProperty("location")
val location: String?,
@JsonProperty("about")
val about: String?,
@JsonProperty("gender")
val gender: String?,
@JsonProperty("age")
val age: Long?,
@JsonProperty("images")
val images: Images?,
) {
data class Ids(
@JsonProperty("slug")
val slug: String,
)

data class Images(
@JsonProperty("avatar")
val avatar: Avatar,
)

data class Avatar(
@JsonProperty("full")
val full: String,
)
}


override suspend fun user(token: AuthToken?): AuthUser? {
if (token == null) return null
// https://trakt.docs.apiary.io/#reference/users/profile/get-user-profile

val userData = app.get(
"$api/users/me?extended=full", headers = getHeaders(token)
).parsed<UserRoot>()

return AuthUser(
name = userData.name,
id = userData.username.hashCode(),
profilePicture = userData.images?.avatar?.full
)
}

override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer =
splitRedirectUrl(redirectUrl)

if (sanitizer["state"] != payload) {
return null
}

// https://trakt.docs.apiary.io/#reference/authentication-oauth/get-token/exchange-code-for-access_token
val tokenData = app.post(
"$api/oauth/token",
json = mapOf(
"code" to (sanitizer["code"] ?: throw ErrorLoadingException("No code")),
"client_id" to id,
"client_secret" to secret,
"redirect_uri" to redirectUri,
"grant_type" to "authorization_code"
)
).parsed<TokenRoot>()

return AuthToken(
accessToken = tokenData.accessToken,
refreshToken = tokenData.refreshToken,
accessTokenLifetime = unixTime + tokenData.expiresIn
)
}

override suspend fun refreshToken(token: AuthToken): AuthToken? {
// https://trakt.docs.apiary.io/#reference/authentication-oauth/get-token/exchange-refresh_token-for-access_token
val tokenData = app.post(
"$api/oauth/token",
json = mapOf(
"refresh_token" to (token.refreshToken
?: throw ErrorLoadingException("No refreshtoken")),
"client_id" to id,
"client_secret" to secret,
"redirect_uri" to redirectUri,
"grant_type" to "refresh_token",
)
).parsed<TokenRoot>()

return AuthToken(
accessToken = tokenData.accessToken,
refreshToken = tokenData.refreshToken,
accessTokenLifetime = unixTime + tokenData.expiresIn
)
}

override fun loginRequest(): AuthLoginPage? {
// https://trakt.docs.apiary.io/#reference/authentication-oauth/authorize/authorize-application
val codeChallenge = generateCodeVerifier()
return AuthLoginPage(
"$mainUrl/oauth/authorize?client_id=$id&response_type=code&redirect_uri=$redirectUri&state=$codeChallenge",
payload = codeChallenge
)
}

data class RatingRoot(
@JsonProperty("rated_at")
val ratedAt: String?,
@JsonProperty("rating")
val rating: Int?,
@JsonProperty("type")
val type: String,
@JsonProperty("season")
val season: Season?,
@JsonProperty("show")
val show: Show?,
@JsonProperty("movie")
val movie: Movie?,
) {
data class Season(
@JsonProperty("number")
val number: Long?,
@JsonProperty("ids")
val ids: Ids?,
)

data class Show(
@JsonProperty("title")
val title: String?,
@JsonProperty("year")
val year: Long?,
@JsonProperty("ids")
val ids: Ids?,
)

data class Movie(
@JsonProperty("title")
val title: String?,
@JsonProperty("year")
val year: Long?,
@JsonProperty("ids")
val ids: Ids?,
)

data class Ids(
@JsonProperty("trakt")
val trakt: String?,
@JsonProperty("slug")
val slug: String?,
@JsonProperty("tvdb")
val tvdb: String?,
@JsonProperty("imdb")
val imdb: String?,
@JsonProperty("tmdb")
val tmdb: String?,
)
}


data class TraktSyncStatus(
override var status: SyncWatchType = SyncWatchType.NONE,
override var score: Score?,
override var watchedEpisodes: Int? = null,
override var isFavorite: Boolean? = null,
override var maxEpisodes: Int? = null,
val type: String,
) : AbstractSyncStatus()

override suspend fun status(token: AuthToken?, id: String): AbstractSyncStatus? {
if (token == null) return null

val response = app.get("$api/sync/ratings/all", headers = getHeaders(token))
.parsed<Array<RatingRoot>>()

// This is criminally wrong, but there is no api to get the rating directly
for (x in response) {
if (x.show?.ids?.trakt == id || x.movie?.ids?.trakt == id || x.season?.ids?.trakt == id) {
return TraktSyncStatus(score = Score.from10(x.rating), type = x.type)
}
}

return SyncStatus(SyncWatchType.NONE, null, null, null, null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.traktApi
import com.lagradost.cloudstream3.syncproviders.AuthLoginResponse
import com.lagradost.cloudstream3.syncproviders.AuthRepo
import com.lagradost.cloudstream3.syncproviders.AuthUser
Expand Down Expand Up @@ -461,6 +462,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback {
R.string.mal_key to SyncRepo(malApi),
R.string.anilist_key to SyncRepo(aniListApi),
R.string.simkl_key to SyncRepo(simklApi),
R.string.trakt_key to SyncRepo(traktApi),
R.string.opensubtitles_key to SubtitleRepo(openSubtitlesApi),
R.string.subdl_key to SubtitleRepo(subDlApi),
)
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/res/drawable/trakt.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:tint="?attr/white"
android:viewportWidth="48"
android:viewportHeight="48">
<!--<path
android:name="path"
android:pathData="M 48 11.26 L 48 36.73 C 48 42.95 42.95 48 36.73 48 L 11.26 48 C 5.04 48 0 42.95 0 36.73 L 0 11.26 C 0 5.04 5.04 0 11.26 0 L 36.73 0 C 40.05 0 43.03 1.43 45.1 3.72 C 45.57 4.24 45.99 4.8 46.35 5.4 C 46.53 5.69 46.69 5.99 46.85 6.29 C 47.18 6.97 47.45 7.68 47.64 8.43 C 47.74 8.8 47.82 9.19 47.87 9.58 C 47.96 10.12 48 10.69 48 11.26 Z"
android:fillColor="#000000"
android:strokeWidth="1"/>-->
<path
android:name="path_1"
android:pathData="M 13.62 17.97 L 21.54 25.89 L 23.01 24.42 L 15.09 16.5 L 13.62 17.97 Z M 28.01 32.37 L 29.48 30.91 L 27.32 28.75 L 47.64 8.43 C 47.45 7.68 47.18 6.97 46.85 6.29 L 24.39 28.75 L 28.01 32.37 Z M 12.92 18.67 L 11.46 20.13 L 25.86 34.53 L 27.32 33.06 L 23 28.75 L 46.35 5.4 C 45.99 4.8 45.57 4.24 45.1 3.72 L 21.54 27.28 L 12.92 18.67 Z M 47.87 9.58 L 28.7 28.75 L 30.17 30.21 L 48 12.38 L 48 11.26 C 48 10.69 47.96 10.12 47.87 9.58 Z M 25.16 22.27 L 17.24 14.35 L 15.77 15.82 L 23.69 23.74 L 25.16 22.27 Z M 41.32 35.12 C 41.32 38.54 38.54 41.32 35.12 41.32 L 12.88 41.32 C 9.46 41.32 6.68 38.54 6.68 35.12 L 6.68 12.88 C 6.68 9.46 9.46 6.67 12.88 6.67 L 33.66 6.67 L 33.66 4.6 L 12.88 4.6 C 8.32 4.6 4.6 8.31 4.6 12.88 L 4.6 35.12 C 4.6 39.68 8.31 43.4 12.88 43.4 L 35.12 43.4 C 39.68 43.4 43.4 39.69 43.4 35.12 L 43.4 31.61 L 41.33 31.61 L 41.33 35.12 Z"
android:fillColor="#ffffff"
android:strokeWidth="1"/>
</vector>
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 @@ -513,6 +513,7 @@
<string name="mal_key" translatable="false">mal_key</string>
<string name="opensubtitles_key" translatable="false">opensubtitles_key</string>
<string name="subdl_key" translatable="false">subdl_key</string>
<string name="trakt_key" translatable="false">trakt_key</string>
<string name="nginx_key" translatable="false">nginx_key</string>
<string name="example_password">password123</string>
<string name="example_username">Username</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/xml/settings_account.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
android:icon="@drawable/simkl_logo"
android:key="@string/simkl_key" />

<Preference
android:icon="@drawable/trakt"
android:key="@string/trakt_key" />

<Preference
android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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.lagradost.cloudstream3.metaproviders.TraktProvider
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.syncproviders.SyncIdName
Expand Down Expand Up @@ -58,7 +59,7 @@ object APIHolder {
get() = System.currentTimeMillis()

// ConcurrentModificationException is possible!!!
val allProviders = threadSafeListOf<MainAPI>()
val allProviders = threadSafeListOf<MainAPI>(TraktProvider())

fun initAll() {
synchronized(allProviders) {
Expand Down Expand Up @@ -1695,6 +1696,7 @@ interface LoadResponse {
var malIdPrefix = "" //malApi.idPrefix
var aniListIdPrefix = "" //aniListApi.idPrefix
var simklIdPrefix = "" //simklApi.idPrefix
var traktIdPrefix = "" //simklApi.idPrefix
var isTrailersEnabled = true

/**
Expand Down Expand Up @@ -1890,7 +1892,7 @@ interface LoadResponse {

@Suppress("UNUSED_PARAMETER")
fun LoadResponse.addTraktId(id: String?) {
// TODO add Trakt sync
this.syncData[traktIdPrefix] = (id ?: return).toString()
}

@Suppress("UNUSED_PARAMETER")
Expand Down
Loading