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

Login with trakt and sync history in movies #24

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8ccb2d5
Add trakt icon in home screen
Oct 4, 2023
cb3624f
Add trakt login module
Oct 4, 2023
4b1e590
Add trakt login module to app
Oct 4, 2023
1d1d8da
Update java version, add Trakt client id from env
Oct 4, 2023
827abb3
Add trakt button for login in home screen and trakt login web view
Oct 4, 2023
25abe17
Add modules
Oct 4, 2023
8bca3c6
add datastore dependency
hadi-norouzi Oct 5, 2023
53fd35f
add TraktAccessToken model
hadi-norouzi Oct 5, 2023
65a55f9
add TraktAuthLocalSource module
hadi-norouzi Oct 5, 2023
db37260
remove deeplink
hadi-norouzi Oct 5, 2023
dd6d732
add trakt network with new retrofit
hadi-norouzi Oct 5, 2023
6beefd4
add TraktAuthRemoteSource module
hadi-norouzi Oct 5, 2023
f58177c
add TraktAuth repository and domain layer
hadi-norouzi Oct 5, 2023
46d3e09
add state for trakt login page to get access token
hadi-norouzi Oct 5, 2023
396555b
add trakt search service
hadi-norouzi Oct 8, 2023
796d754
add trakt search remote source
hadi-norouzi Oct 8, 2023
7c0a70f
combine trakt search id with tmdb get movie detail
hadi-norouzi Oct 8, 2023
0976e6b
Merge branch 'dev' into feature/trakt
hadi-norouzi Oct 8, 2023
316d766
spotless apply formats and remove test
hadi-norouzi Oct 8, 2023
a89f16e
#18 Automatically add pre-commit hook for spotlessCheck
moallemi Oct 8, 2023
08a6128
update package directory
hadi-norouzi Oct 9, 2023
28ce8ff
add watched button
hadi-norouzi Oct 9, 2023
b258d08
fix trakt auth local source with nullable tokens
Oct 9, 2023
67752e5
add trakt auth state use case
Oct 9, 2023
d5238b6
show green icon when user logged in with trakt
Oct 9, 2023
d916d46
Merge remote-tracking branch 'origin/feature/trakt' into feature/trakt
hadi-norouzi Oct 9, 2023
a362661
reformat codes
hadi-norouzi Oct 9, 2023
35f7a36
Merge branch 'dev' of https://github.com/hadi-norouzi/Film-Time into …
hadi-norouzi Oct 9, 2023
dfce754
add initial state for trakt auth state
hadi-norouzi Oct 9, 2023
8d6ccfb
add watched button
hadi-norouzi Oct 27, 2023
46c97a1
add trakt sync api
hadi-norouzi Oct 27, 2023
e38a446
add trakt history usecase
hadi-norouzi Oct 27, 2023
af0cf85
Add: added to history button and implement trakt history sync api
hadi-norouzi Oct 28, 2023
15b8e91
implement trakt add to history functionality
hadi-norouzi Oct 28, 2023
d1d1191
format codes
hadi-norouzi Oct 28, 2023
6594041
update api keys readme
hadi-norouzi Oct 30, 2023
3bd66f3
Merge remote-tracking branch 'origin/feature/trakt' into feature/trakt
hadi-norouzi Oct 30, 2023
f7a8b72
trakt remote source error handling
hadi-norouzi Nov 5, 2023
00af18d
trakt sync remote source error handling
hadi-norouzi Nov 5, 2023
30b068f
fix lints
hadi-norouzi Nov 5, 2023
be3eaab
Merge remote-tracking branch 'origin/dev' into feature/trakt
hadi-norouzi Nov 5, 2023
57ae990
fix lints
hadi-norouzi Nov 5, 2023
49f4fed
Merge remote-tracking branch 'origin/dev' into feature/trakt
hadi-norouzi Dec 31, 2023
be7a7b3
remove prints
hadi-norouzi Dec 31, 2023
201972e
add: trakt header interceptor
hadi-norouzi Dec 31, 2023
54f097b
add: TODO issue references
hadi-norouzi Dec 31, 2023
45c5d0b
add: issue references
hadi-norouzi Dec 31, 2023
edf8f6a
remove: api headers annotations
hadi-norouzi Dec 31, 2023
2a3bec4
add: issue reference
hadi-norouzi Dec 31, 2023
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
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,
moallemi marked this conversation as resolved.
Show resolved Hide resolved
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 = "",
)
Comment on lines +20 to +25
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create another issue for extracting this code as a new function

return when (result) {
is NetworkResponse.ApiError -> TODO()
is NetworkResponse.NetworkError -> TODO()
is NetworkResponse.Success -> TODO()
is NetworkResponse.UnknownError -> TODO()
}
}
Comment on lines +27 to +32
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create another issue for handling results


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,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this access token to an okttp interceptor

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to depend network module on LocalStorage module to read stored tokes?

)
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