Skip to content

Commit

Permalink
#24 Login with trakt and sync history in movies
Browse files Browse the repository at this point in the history
  • Loading branch information
hadi-norouzi committed Jan 8, 2024
1 parent 8b98fee commit 44dffa6
Show file tree
Hide file tree
Showing 95 changed files with 1,810 additions and 17 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ You need to supply API / client keys for the various services the
app uses:

- [TMDb](https://developers.themoviedb.org)
- [Trakt](https://trakt.tv/oauth/applications)

You can find information about how to gain access [here](docs/API-Keys.md).

Expand All @@ -58,6 +59,9 @@ Add this to your system environment variables:
```shell
# Get this from TMDb
FILM_TIME_TMDB_API_KEY=<insert>
# Get these from Trakt
FILM_TIME_TRAKT_CLIENT_ID=<insert>
FILM_TIME_TRAKT_CLIENT_SECRET=<insert>
```

Do not forget to restart Android Studio to apply changes to your environment.
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dependencies {
implementation(project(":feature:show-detail"))
implementation(project(":feature:home"))
implementation(project(":feature:player"))
implementation(project(":feature:trakt-login"))

implementation(libs.core.ktx)
implementation(libs.lifecycle.runtime.ktx)
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/io/filmtime/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import io.filmtime.feature.home.HomeScreen
import io.filmtime.feature.movie.detail.MovieDetailScreen
import io.filmtime.feature.player.VideoPlayer
import io.filmtime.feature.show.detail.ShowDetailScreen
import io.filmtime.feature.trakt.login.TraktLoginWebView
import io.filmtime.ui.theme.FilmTimeTheme

@AndroidEntryPoint
Expand All @@ -40,6 +41,9 @@ class MainActivity : ComponentActivity() {
onShowClick = { tmdbId ->
navController.navigate("show/detail/$tmdbId")
},
onTraktClick = {
navController.navigate("trakt/login")
},
)
}
composable(
Expand Down Expand Up @@ -86,6 +90,19 @@ class MainActivity : ComponentActivity() {
viewModel = hiltViewModel(),
)
}
composable(
route = "trakt/login",
) {
TraktLoginWebView(
viewModel = hiltViewModel(),
onBackPressed = {
navController.popBackStack()
},
onSuccess = {
navController.popBackStack()
},
)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions data/api/trakt/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
54 changes: 54 additions & 0 deletions data/api/trakt/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
alias(libs.plugins.com.android.library)
alias(libs.plugins.org.jetbrains.kotlin.android)

kotlin("kapt")
alias(libs.plugins.hilt.android)
}

android {
namespace = "io.filmtime.data.api.trakt"
compileSdk = 34

defaultConfig {
minSdk = 27

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}

dependencies {

implementation(project(":data:model"))
implementation(project(":data:network"))
implementation(project(":data:storage:trakt"))

implementation(libs.hilt.android)
kapt(libs.dagger.hilt.android.compiler)

implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
}
Empty file.
21 changes: 21 additions & 0 deletions data/api/trakt/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions data/api/trakt/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.TraktTokens
import io.filmtime.data.network.trakt.TraktAccessTokenResponse

fun TraktAccessTokenResponse.toAccessToken() =
TraktTokens(
accessToken = accessToken,
refreshToken = refreshToken,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result
import io.filmtime.data.model.TraktTokens

interface TraktAuthRemoteSource {

suspend fun getAccessToken(code: String): Result<TraktTokens, GeneralError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result
import io.filmtime.data.model.TraktTokens
import io.filmtime.data.network.BuildConfig
import io.filmtime.data.network.adapter.NetworkResponse
import io.filmtime.data.network.trakt.TraktAuthService
import io.filmtime.data.network.trakt.TraktGetTokenRequest
import javax.inject.Inject

class TraktAuthRemoteSourceImpl @Inject constructor(
private val traktAuthService: TraktAuthService,
) : TraktAuthRemoteSource {

override suspend fun getAccessToken(code: String): Result<TraktTokens, GeneralError> {
val result = traktAuthService.getAccessToken(
body = TraktGetTokenRequest(
code = code,
clientID = BuildConfig.TRAKT_CLIENT_ID,
clientSecret = BuildConfig.TRAKT_CLIENT_SECRET,
grantType = "authorization_code",
redirectURI = "filmtime://",
),
)
return when (result) {
is NetworkResponse.ApiError -> Result.Failure(GeneralError.ApiError(result.body.error, result.code))
is NetworkResponse.NetworkError -> Result.Failure(GeneralError.NetworkError)
is NetworkResponse.Success -> {
val response = result.body
if (response == null) {
Result.Failure(GeneralError.UnknownError(Throwable("Access token response is null")))
} else {
Result.Success(response.toAccessToken())
}
}
is NetworkResponse.UnknownError -> Result.Failure(GeneralError.UnknownError(result.error))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result

enum class TmdbType {
MOVIE,
SHOW,
// EPISODE,
// PERSON,
}

interface TraktSearchRemoteSource {

suspend fun getByTmdbId(id: String, type: TmdbType? = null): Result<Long, GeneralError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result
import io.filmtime.data.network.adapter.NetworkResponse
import io.filmtime.data.network.trakt.TraktSearchService
import javax.inject.Inject

class TraktSearchRemoteSourceImpl @Inject constructor(
private val traktIDLookupService: TraktSearchService,
) : TraktSearchRemoteSource {
override suspend fun getByTmdbId(id: String, type: TmdbType?): Result<Long, GeneralError> {
return when (val result = traktIDLookupService.movieIDLookup(idType = "tmdb", id = id)) {
is NetworkResponse.ApiError -> TODO() // Will be handled in #31
is NetworkResponse.NetworkError -> Result.Failure(GeneralError.NetworkError)
is NetworkResponse.Success -> {
val body = result.body ?: emptyList()
val movieItemId = body.find { it.movie.ids.tmdb == id.toLong() }?.movie?.ids?.trakt ?: -1
return Result.Success(movieItemId)
}

is NetworkResponse.UnknownError -> TODO() // Will be handled in #31
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.filmtime.data.api.trakt

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@InstallIn(SingletonComponent::class)
@Module
abstract class TraktSourceModule {

@Binds
abstract fun bindsTraktAuthRemoteSource(
sourceImpl: TraktAuthRemoteSourceImpl,
): TraktAuthRemoteSource

@Binds
abstract fun bindsTraktSearchRemoteSource(
sourceImpl: TraktSearchRemoteSourceImpl,
): TraktSearchRemoteSource

@Binds
abstract fun bindsTraktSyncRemoteSource(
sourceImpl: TraktSyncRemoteSourceImpl,
): TraktSyncRemoteSource
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result

interface TraktSyncRemoteSource {

suspend fun getAllHistories(): Result<Nothing, GeneralError>

suspend fun getHistoryById(id: String): Result<Boolean, GeneralError>

suspend fun addToHistory(id: String): Result<Unit, GeneralError>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.filmtime.data.api.trakt

import io.filmtime.data.model.GeneralError
import io.filmtime.data.model.Result
import io.filmtime.data.network.adapter.NetworkResponse
import io.filmtime.data.network.trakt.AddHistoryRequest
import io.filmtime.data.network.trakt.HistoryIDS
import io.filmtime.data.network.trakt.MovieHistory
import io.filmtime.data.network.trakt.TraktSyncService
import io.filmtime.data.storage.trakt.TraktAuthLocalSource
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject

class TraktSyncRemoteSourceImpl
@Inject constructor(
private val traktSyncService: TraktSyncService,
private val traktAuthLocalSource: TraktAuthLocalSource,
) : TraktSyncRemoteSource {
override suspend fun getAllHistories(): Result<Nothing, GeneralError> {
// TODO: move check token in a function
traktAuthLocalSource.tokens.firstOrNull() ?: return Result.Failure(GeneralError.ApiError("Unauthorized", 401))
val result = traktSyncService.getWatchedHistory(
type = "movies",
accessToken = "",
)
return when (result) {
is NetworkResponse.ApiError -> TODO()
is NetworkResponse.NetworkError -> TODO()
is NetworkResponse.Success -> TODO()
is NetworkResponse.UnknownError -> TODO()
}
}

override suspend fun getHistoryById(id: String): Result<Boolean, GeneralError> {
// TODO: move check token in a function
val tokens =
traktAuthLocalSource.tokens.firstOrNull() ?: return Result.Failure(GeneralError.ApiError("Unauthorized", 401))
val result = traktSyncService.getHistoryById(
type = "movies",
id = id,
accessToken = "Bearer " + tokens.accessToken,
)
return when (result) {
is NetworkResponse.ApiError -> Result.Failure(GeneralError.ApiError(result.body.error, result.code))
is NetworkResponse.NetworkError -> Result.Failure(GeneralError.NetworkError)
is NetworkResponse.UnknownError -> Result.Failure(GeneralError.UnknownError(result.error))
is NetworkResponse.Success -> {
val watched = result.body?.any { it.movie.ids.trakt == id.toLong() } ?: false
Result.Success(watched)
}
}
}

override suspend fun addToHistory(id: String): Result<Unit, GeneralError> {
val tokens =
traktAuthLocalSource.tokens.firstOrNull() ?: return Result.Failure(GeneralError.ApiError("Unauthorized", 401))
val result = traktSyncService.addMovieToHistory(
accessToken = "Bearer " + tokens.accessToken,
body = AddHistoryRequest(
movies = listOf(
MovieHistory(
ids = HistoryIDS(
trakt = id.toLong(),
),
),
),
),
)
return when (result) {
is NetworkResponse.ApiError -> Result.Failure(GeneralError.ApiError(result.body.error, result.code))
is NetworkResponse.NetworkError -> Result.Failure(GeneralError.NetworkError)
is NetworkResponse.UnknownError -> Result.Failure(GeneralError.UnknownError(result.error))
is NetworkResponse.Success -> Result.Success(Unit)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.filmtime.data.model

data class TraktTokens(
val accessToken: String,
val refreshToken: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ data class VideoDetail(
val originalLanguage: String?,
val spokenLanguages: List<String>,
val description: String,
val isWatched: Boolean? = null,
)
2 changes: 2 additions & 0 deletions data/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ android {
minSdk = 27

buildConfigField("String", "TMDB_API_KEY", "\"${System.getenv("FILM_TIME_TMDB_API_KEY")}\"")
buildConfigField("String", "TRAKT_CLIENT_ID", "\"${System.getenv("FILM_TIME_TRAKT_CLIENT_ID")}\"")
buildConfigField("String", "TRAKT_CLIENT_SECRET", "\"${System.getenv("FILM_TIME_TRAKT_CLIENT_SECRET")}\"")

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
Expand Down
Loading

0 comments on commit 44dffa6

Please sign in to comment.