Skip to content

Commit

Permalink
Support TMDB account endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisKruegerDev committed Mar 6, 2023
1 parent 1aad1c7 commit 5f845fe
Show file tree
Hide file tree
Showing 38 changed files with 666 additions and 164 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ val result = tmdb.discover.discoverByCategory(
)
```

### Authentication
Build up your authentication URL, which you can open in the web view.

```kotlin
// Returns "https://www.themoviedb.org/authenticate/[request_token]?redirect_to=auth://yourApp"
val url = tmdb.authentication.requestAuthorizationUrl("auth://yourApp")
```
After opening the URL, TMDB version 3 returns the following URL:
```kotlin
yourApp://auth/login?request_token=[request_token]&approved=true
```


### Build image URL

You can build an image URL via the poster file path and size key. More information on the [TMDb images site](https://developers.themoviedb.org/3/getting-started/images).
Expand Down
13 changes: 10 additions & 3 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb3.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fun Tmdb3(block: TmdbClientConfig.() -> Unit): Tmdb3 {

class Tmdb3 internal constructor(private val config: TmdbClientConfig) {

constructor(tmdbApiKey: String) : this(TmdbClientConfig.buildDefault(tmdbApiKey))
constructor(tmdbApiKey: String) : this(TmdbClientConfig.withKey(tmdbApiKey))

init {
requireNotNull(config.tmdbApiKey) {
Expand All @@ -51,8 +51,15 @@ class Tmdb3 internal constructor(private val config: TmdbClientConfig) {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbCredentials?.sessionIdProvider?.invoke()?.let { sessionId ->
it.parameter(TmdbUrlParameter.SESSION_ID, sessionId)
val pathSegments = it.url.pathSegments.toSet()
if (pathSegments.contains("account") || pathSegments.contains("authentication")) {
config.tmdbAuthCredentials?.sessionIdProvider?.invoke()?.let { sessionId ->
it.parameter(TmdbUrlParameter.SESSION_ID, sessionId)
}
}

config.tmdbAuthCredentials?.guestSessionIdProvider?.invoke()?.let { sessionId ->
it.parameter(TmdbUrlParameter.GUEST_SESSION_ID, sessionId)
}
}
}
Expand Down
24 changes: 10 additions & 14 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ class Tmdb4 internal constructor(
private val config: TmdbClientConfig
) {

constructor(
tmdbApiKey: String,
authenticationToken: String? = null
) : this(TmdbClientConfig.buildDefault(tmdbApiKey, authenticationToken))
constructor(tmdbApiKey: String) : this(TmdbClientConfig.withKey(tmdbApiKey))

init {
check(!config.tmdbApiKey.isNullOrBlank()) {
Expand All @@ -31,31 +28,30 @@ class Tmdb4 internal constructor(
}

private val client by lazy {
HttpClientFactory.buildHttpClient(TmdbVersion.V4, config).apply {
HttpClientFactory.buildHttpClient(
version = TmdbVersion.V4,
config = config,
useAuthentication = true
).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbCredentials?.accessTokenProvider?.let { token ->
it.header("Authorization", "Bearer $token")
}
}
}
}

private val authClient by lazy {
private val clientForAuth by lazy {
HttpClientFactory.buildHttpClient(TmdbVersion.V4, config).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbAuthenticationToken?.let { token ->
it.header("Authorization", "Bearer $token")
config.tmdbAuthCredentials?.authenticationToken?.let { token ->
it.bearerAuth(token)
}
}
}
}

val account by buildApi(::Tmdb4AccountApi)
val auth by lazy { Tmdb4AuthenticationApi(authClient) }
val auth by lazy { Tmdb4AuthenticationApi(clientForAuth) }
val list by buildApi(::Tmdb4ListApi)

private inline fun <T> buildApi(crossinline builder: (HttpClient) -> T) = lazy { builder(client) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import io.ktor.client.plugins.logging.*
class TmdbClientConfig {

var tmdbApiKey: String? = null
var tmdbAuthenticationToken: String? = null
internal var tmdbCredentials: TmdbCredentials? = null
internal var tmdbAuthCredentials: TmdbAuthCredentials? = null

var expectSuccess: Boolean = true
var useCache: Boolean = false
Expand All @@ -21,8 +20,8 @@ class TmdbClientConfig {
internal var httpClientBuilder: (() -> HttpClient)? = null
internal var httpClientLoggingBlock: (Logging.Config.() -> Unit)? = null

fun tmdbAccountCredentials(block: TmdbCredentials.() -> Unit) {
tmdbCredentials = TmdbCredentials().apply(block)
fun userAuthentication(block: TmdbAuthCredentials.() -> Unit) {
tmdbAuthCredentials = TmdbAuthCredentials().apply(block)
}

fun logging(block: Logging.Config.() -> Unit) {
Expand Down Expand Up @@ -51,32 +50,31 @@ class TmdbClientConfig {

companion object {

internal fun buildDefault(
tmdbApiKey: String,
tmdbAuthenticationToken: String? = null
) = TmdbClientConfig().apply {
internal fun withKey(tmdbApiKey: String) = TmdbClientConfig().apply {
this.tmdbApiKey = tmdbApiKey
this.tmdbAuthenticationToken = tmdbAuthenticationToken
}
}
}

@TmdbDsl
class TmdbCredentials {
class TmdbAuthCredentials {

// used in version 4
var authenticationToken: String? = null

internal var sessionIdProvider: (() -> String?)? = null
internal var guestSessionIdProvider: (() -> String?)? = null
internal var accessTokenProvider: (() -> String?)? = null
internal var requestTokenProvider: (() -> String?)? = null

fun sessionId(provider: () -> String?) {
fun loadSessionId(provider: () -> String?) {
sessionIdProvider = provider
}

fun accessToken(provider: () -> String?) {
accessTokenProvider = provider
fun loadGuestSessionId(provider: () -> String?) {
guestSessionIdProvider = provider
}

fun requestToken(provider: () -> String?) {
requestTokenProvider = provider
fun loadAccessToken(provider: () -> String?) {
accessTokenProvider = provider
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package app.moviebase.tmdb

internal object TmdbWebConfig {
const val TMDB_HOST = "api.themoviedb.org"

const val BASE_URL_TMDB = "https://api.themoviedb.org"
const val WEBSITE_BASE_URL = "https://www.themoviedb.org"
const val BASE_WEBSITE_URL = "https://www.themoviedb.org"

const val BASE_URL_TMDB_IMAGE = "https://image.tmdb.org/t/p/"
const val BASE_URL_YOUTUBE_IMAGE = "https://img.youtube.com/vi"
Expand All @@ -15,6 +17,7 @@ internal object TmdbWebConfig {
internal object TmdbUrlParameter {
const val API_KEY = "api_key"
const val SESSION_ID = "session_id"
const val GUEST_SESSION_ID = "guest_session_id"
const val ACCESS_TOKEN = "access_token"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Tmdb4AccountApi internal constructor(private val client: HttpClient) {

/**
* Get all of the lists you've created.
*
* @see [Documentation] (https://developers.themoviedb.org/4/account/get-account-lists)
*/
suspend fun getLists(accountId: String, page: Int): TmdbListMetaPageResult = client.get {
endPointAccount(accountId, "lists")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,40 @@ import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

/**
* User Authorization
*
* @see [Documentation] (https://developers.themoviedb.org/4/auth/user-authorization-1)
*/
class Tmdb4AuthenticationApi internal constructor(private val client: HttpClient) {

suspend fun requestToken(auth: Tmdb4RedirectToBodyAuth): TmdbListMetaPageResult = client.post {
/**
* This method generates a new request token that you can ask a user to approve.
* @see [Documentation] (https://developers.themoviedb.org/4/auth/create-request-token)
*/
suspend fun requestToken(auth: Tmdb4RedirectToBodyAuth): Tmdb4RequestToken = client.post {
endPointV4("auth", "request_token")
json()
setBody(auth)
}.body()

suspend fun accessToken(requestToken: Tmdb4RequestTokenBody): TmdbListMetaPageResult = client.post {
/**
* This method will finish the user authentication flow and issue an official user access token.
* @see [Documentation] (https://developers.themoviedb.org/4/auth/create-access-token)
*/
suspend fun accessToken(requestToken: Tmdb4RequestTokenBody): Tmdb4AccessToken = client.post {
endPointV4("auth", "access_token")
json()
setBody(requestToken)
}.body()

/**
* This method gives your users the ability to log out of a session.
* @see [Documentation] (https://developers.themoviedb.org/4/auth/delete-access-token)
*/
suspend fun deleteAccessToken(deleteAccessToken: Tmdb4DeleteAccessTokenBody): Tmdb4AccessToken = client.delete {
endPointV4("auth", "access_token")
json()
setBody(deleteAccessToken)
}.body()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Tmdb4ListApi internal constructor(private val client: HttpClient) {

/**
* This method will retrieve a list by id.
* Private lists can only be accessed by their owners and therefore require a valid user access token.
* Note: Private lists can only be accessed by their owners and therefore require a valid user access token.
*
* @see [Documentation] (https://developers.themoviedb.org/4/list/get-list)
*/
suspend fun getList(
listId: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package app.moviebase.tmdb.api

import app.moviebase.tmdb.model.TmdbAccountDetails
import app.moviebase.tmdb.model.TmdbFavoriteRequestBody
import app.moviebase.tmdb.model.TmdbWatchlistRequestBody
import app.moviebase.tmdb.model.*
import app.moviebase.tmdb.remote.endPointV3
import app.moviebase.tmdb.remote.json
import io.ktor.client.*
Expand All @@ -11,28 +9,54 @@ import io.ktor.client.request.*

class TmdbAccountApi internal constructor(private val client: HttpClient) {

suspend fun getDetails(sessionId: String): TmdbAccountDetails = client.get {
/**
* Get your account details.
* @see [Documentation](https://developers.themoviedb.org/3/account)
*/
suspend fun getDetails(): TmdbAccountDetails = client.get {
endPointV3("account")
parameter("session_id", sessionId)
}.body()

suspend fun markFavorite(accountId: String, requestBody: TmdbFavoriteRequestBody): TmdbAccountDetails = client.post {
endPointAccount(accountId, "favorite")
parameter("account_id", accountId)
json()
suspend fun getFavorites(accountId: Int, mediaType: TmdbMediaType): TmdbMoviePageResult = when (mediaType) {
TmdbMediaType.MOVIE -> getFavoriteMovies(accountId)
TmdbMediaType.SHOW -> getFavoriteShows(accountId)
else -> throw IllegalArgumentException("Only movies and shows are supported.")
}

setBody(requestBody)
suspend fun getFavoriteMovies(accountId: Int): TmdbMoviePageResult = client.get {
endPointAccount(accountId, "favorite", "movies")
}.body()

suspend fun getFavoriteShows(accountId: Int): TmdbMoviePageResult = client.get {
endPointAccount(accountId, "favorite", "tv")
}.body()

suspend fun markWatchlist(accountId: String, requestBody: TmdbWatchlistRequestBody): TmdbAccountDetails = client.post {
/**
* POST /account/{account_id}/favorite
*
* This method allows you to mark a movie or TV show as a favorite item.
* @see [Documentation](https://developers.themoviedb.org/3/account/mark-as-favorite)
*/
suspend fun markFavorite(accountId: Int, requestBody: TmdbFavoriteRequestBody): TmdbStatusResult = client.post {
endPointAccount(accountId, "favorite")
parameter("account_id", accountId)
json()
setBody(requestBody)
}.body()

suspend fun markFavorite(
accountId: Int,
mediaType: TmdbMediaType,
mediaId: Int,
favorite: Boolean
): TmdbStatusResult = markFavorite(accountId, TmdbFavoriteRequestBody(mediaType, mediaId, favorite))

suspend fun addWatchlist(accountId: Int, requestBody: TmdbWatchlistRequestBody): TmdbStatusResult = client.post {
endPointAccount(accountId, "watchlist")
json()
setBody(requestBody)
}.body()

private fun HttpRequestBuilder.endPointAccount(accountId: String, vararg paths: String) {
endPointV3("account", accountId, *paths)
private fun HttpRequestBuilder.endPointAccount(accountId: Int, vararg paths: String) {
endPointV3("account", accountId.toString(), *paths)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.moviebase.tmdb.model.TmdbGuestSession
import app.moviebase.tmdb.model.TmdbRequestToken
import app.moviebase.tmdb.model.TmdbSession
import app.moviebase.tmdb.remote.endPointV3
import app.moviebase.tmdb.url.TmdbAuthenticationUrlBuilder
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
Expand Down Expand Up @@ -31,15 +32,37 @@ class TmdbAuthenticationApi internal constructor(private val client: HttpClient)
endPointV3("authentication/guest_session/new")
}.body()

suspend fun acquireAccountSession(userName: String, password: String): String {
/**
* @return the sessionId or null the request was if unsuccessful
*/
suspend fun acquireAccountSession(userName: String, password: String): String? {
var token = requestToken()
token = validateToken(userName, password, token.requestToken)
if (!token.success) return null

val session = createSession(token.requestToken)
if (!session.success) return null

return session.sessionId
}

suspend fun requestAuthorizationUrl(redirectTo: String): String {
val requestToken = requestToken().requestToken
return "https://www.themoviedb.org/authenticate/${requestToken}?redirect_to=$redirectTo"
suspend fun acquireGuestSession(userName: String, password: String): String? {
val session = createGuestSession()
if (!session.success) return null

return session.guestSessionId
}

/**
* @return the authorization TMDB URL or null if request was unsuccessful
*/
suspend fun requestAuthorizationUrl(redirectTo: String): String? {
val requestToken = requestToken()
val requestTokenValue = requestToken.requestToken
return if (requestToken.success) {
TmdbAuthenticationUrlBuilder.buildAuthorizationUrl(requestTokenValue, redirectTo)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package app.moviebase.tmdb.api

import io.ktor.client.*

/**
* Fetches the Lists form TMDB version 3.
* Note: When to use version 4, see [Documentation] (https://developers.themoviedb.org/3/lists/v4-or-v3-lists)
*/
class TmdbListsApi internal constructor(private val client: HttpClient) {


Expand Down
Loading

0 comments on commit 5f845fe

Please sign in to comment.