Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TMDB account endpoints #37

Merged
merged 1 commit into from
Mar 6, 2023
Merged
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
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