Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2017-2025 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2017 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.jobs

Expand Down Expand Up @@ -125,8 +125,8 @@ abstract class BaseNetworkJob(
SgTrakt.checkForTraktError(trakt, response)
)
val resultCode = when {
SgTrakt.isRateLimitExceeded(response) || SgTrakt.isServerError(response) -> ERROR_TRAKT_SERVER
SgTrakt.isAccountLimitExceeded(response) -> ERROR_TRAKT_ACCOUNT_LIMIT_EXCEEDED
TraktV2.isRateLimitExceeded(response) || TraktV2.isServerError(response) -> ERROR_TRAKT_SERVER
TraktV2.isAccountLimitExceeded(response) -> ERROR_TRAKT_ACCOUNT_LIMIT_EXCEEDED
TraktV2.isAccountLocked(response) -> ERROR_TRAKT_ACCOUNT_LOCKED
else -> ERROR_TRAKT_CLIENT
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015-2024 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2015 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.shows.search.discover

Expand All @@ -8,14 +8,13 @@ import androidx.annotation.StringRes
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.SgApp
import com.battlelancer.seriesguide.shows.ShowsSettings
import com.battlelancer.seriesguide.traktapi.SgTrakt
import com.battlelancer.seriesguide.util.Errors
import com.battlelancer.seriesguide.traktapi.TraktTools4
import com.battlelancer.seriesguide.traktapi.TraktTools4.TraktNonNullResponse.Success
import com.uwetrottmann.androidutils.AndroidUtils
import com.uwetrottmann.androidutils.GenericSimpleLoader
import com.uwetrottmann.trakt5.TraktV2
import com.uwetrottmann.trakt5.entities.BaseShow
import com.uwetrottmann.trakt5.enums.Extended
import retrofit2.Response
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.util.LinkedList

/**
Expand Down Expand Up @@ -48,46 +47,28 @@ class TraktAddLoader(
private val trakt: TraktV2 = SgApp.getServicesComponent(context).trakt()

override fun loadInBackground(): Result {
var shows: List<BaseShow> = emptyList()
var action: String? = null
try {
val response: Response<List<BaseShow>>
val response = runBlocking(Dispatchers.Default) {
val traktSync = trakt.sync()
when (type) {
Type.WATCHED -> {
action = "load watched shows"
response = trakt.sync().watchedShows(Extended.NOSEASONS).execute()
}

Type.COLLECTION -> {
action = "load show collection"
response = trakt.sync().collectionShows(null).execute()
}
Type.WATCHED -> TraktTools4.getWatchedShows(traktSync, noSeasons = true)
Type.COLLECTION -> TraktTools4.getCollectedShows(traktSync)
Type.WATCHLIST -> TraktTools4.getShowsOnWatchlist(traktSync)
}
}

Type.WATCHLIST -> {
action = "load show watchlist"
response = trakt.sync().watchlistShows(Extended.FULL).execute()
}
val shows = when (response) {
is Success -> response.data
is TraktTools4.TraktErrorResponse.IsUnauthorized -> {
return buildResultFailure(R.string.trakt_error_credentials)
}

if (response.isSuccessful) {
val body = response.body()
if (body != null) {
shows = body
else -> {
// Wait to check for network until here to allow hitting the response cache
return if (AndroidUtils.isNetworkConnected(context)) {
buildResultGenericFailure()
} else {
buildResultFailure(R.string.offline)
}
} else {
if (SgTrakt.isUnauthorized(context, response)) {
return buildResultFailure(R.string.trakt_error_credentials)
}
Errors.logAndReport(action, response)
return buildResultGenericFailure()
}
} catch (e: Exception) {
Errors.logAndReport(action!!, e)
// only check for network here to allow hitting the response cache
return if (AndroidUtils.isNetworkConnected(context)) {
buildResultGenericFailure()
} else {
buildResultFailure(R.string.offline)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2011-2025 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2011 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.shows.tools

Expand All @@ -11,13 +11,13 @@ import com.battlelancer.seriesguide.SgApp
import com.battlelancer.seriesguide.backend.settings.HexagonSettings
import com.battlelancer.seriesguide.provider.SeriesGuideDatabase
import com.battlelancer.seriesguide.provider.SgRoomDatabase
import com.battlelancer.seriesguide.shows.tools.AddShowTask.OnShowAddedEvent
import com.battlelancer.seriesguide.shows.tools.AddUpdateShowTools.ShowResult
import com.battlelancer.seriesguide.sync.HexagonEpisodeSync
import com.battlelancer.seriesguide.traktapi.TraktCredentials
import com.battlelancer.seriesguide.traktapi.TraktSettings
import com.battlelancer.seriesguide.traktapi.TraktTools2
import com.battlelancer.seriesguide.traktapi.TraktTools2.ServiceResult
import com.battlelancer.seriesguide.traktapi.TraktTools4
import com.battlelancer.seriesguide.traktapi.TraktTools4.TraktErrorResponse.IsUnauthorized
import com.battlelancer.seriesguide.traktapi.TraktTools4.TraktNonNullResponse.Success
import com.battlelancer.seriesguide.util.Errors
import com.uwetrottmann.androidutils.AndroidUtils
import com.uwetrottmann.trakt5.entities.BaseShow
Expand Down Expand Up @@ -114,7 +114,7 @@ class AddShowTask(
addQueue.addAll(shows)
}

fun run() {
suspend fun run() {
Timber.d("Starting to add shows...")

val firstShow = addQueue.peek()
Expand Down Expand Up @@ -298,15 +298,30 @@ class AddShowTask(
publishProgress(result, 0, "")
}

private fun getTraktShows(isCollectionNotWatched: Boolean): Map<Int, BaseShow>? {
val result: Pair<Map<Int, BaseShow>?, ServiceResult> =
TraktTools2.getCollectedOrWatchedShows(isCollectionNotWatched, context)
if (result.second == ServiceResult.AUTH_ERROR) {
publishProgress(RESULT_TRAKT_AUTH_ERROR)
} else if (result.second == ServiceResult.API_ERROR) {
publishProgress(RESULT_TRAKT_API_ERROR)
private suspend fun getTraktShows(isCollectionNotWatched: Boolean): Map<Int, BaseShow>? {
val traktSync = SgApp.getServicesComponent(context).traktSync()!!

val response =
if (isCollectionNotWatched) {
TraktTools4.getCollectedShowsByTmdbId(traktSync)
} else {
TraktTools4.getWatchedShowsByTmdbId(traktSync)
}

when (response) {
is Success -> {
return response.data
}

is IsUnauthorized -> {
publishProgress(RESULT_TRAKT_AUTH_ERROR)
}

else -> {
publishProgress(RESULT_TRAKT_API_ERROR)
}
}
return result.first
return null
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2017-2024 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2017 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.sync

Expand All @@ -12,10 +12,10 @@ import com.battlelancer.seriesguide.shows.database.SgEpisode2ForSync
import com.battlelancer.seriesguide.shows.database.SgEpisode2WatchedUpdate
import com.battlelancer.seriesguide.shows.episodes.EpisodeFlags
import com.battlelancer.seriesguide.shows.episodes.EpisodeTools
import com.battlelancer.seriesguide.traktapi.SgTrakt
import com.battlelancer.seriesguide.traktapi.TraktSettings
import com.battlelancer.seriesguide.traktapi.TraktTools
import com.battlelancer.seriesguide.traktapi.TraktTools2
import com.battlelancer.seriesguide.traktapi.TraktTools4
import com.battlelancer.seriesguide.traktapi.TraktTools4.TraktNonNullResponse.Success
import com.battlelancer.seriesguide.util.Errors
import com.battlelancer.seriesguide.util.TimeTools
import com.uwetrottmann.trakt5.entities.BaseSeason
Expand All @@ -25,6 +25,8 @@ import com.uwetrottmann.trakt5.entities.SyncEpisode
import com.uwetrottmann.trakt5.entities.SyncItems
import com.uwetrottmann.trakt5.entities.SyncSeason
import com.uwetrottmann.trakt5.entities.SyncShow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.threeten.bp.OffsetDateTime
import timber.log.Timber

Expand Down Expand Up @@ -63,7 +65,11 @@ class TraktEpisodeSync(
* Trakt. If an episode has multiple plays, uploads it multiple times.
* If false, sets episodes that are not watched on Trakt but watched locally
* (and only those, e.g. no skipped episodes) as not watched.
*
* Note: this uses [runBlocking], so if the calling thread is interrupted this will throw
* [InterruptedException].
*/
@Throws(InterruptedException::class)
fun syncWatched(
tmdbIdsToShowIds: Map<Int, Long>,
watchedAt: OffsetDateTime?,
Expand All @@ -75,21 +81,11 @@ class TraktEpisodeSync(
}
val lastWatchedAt = TraktSettings.getLastEpisodesWatchedAt(context)
if (isInitialSync || TimeTools.isAfterMillis(watchedAt, lastWatchedAt)) {
val watchedShowsTrakt = try {
val response = traktSync!!.sync
.watchedShows(null)
.execute()
if (!response.isSuccessful) {
if (SgTrakt.isUnauthorized(context, response)) {
return false
}
Errors.logAndReport("get watched shows", response)
return false
val watchedShowsTrakt = runBlocking(Dispatchers.Default) {
when (val response = TraktTools4.getWatchedShowsByTmdbId(traktSync!!.sync)) {
is Success -> response.data
else -> null
}
response.body()
} catch (e: Exception) {
Errors.logAndReport("get watched shows", e)
return false
} ?: return false

// apply database updates, if initial sync upload diff
Expand Down Expand Up @@ -124,7 +120,11 @@ class TraktEpisodeSync(
* Trakt.
* If false, sets episodes that are not collected on Trakt but collected locally
* as not collected.
*
* Note: this uses [runBlocking], so if the calling thread is interrupted this will throw
* [InterruptedException].
*/
@Throws(InterruptedException::class)
fun syncCollected(
tmdbIdsToShowIds: Map<Int, Long>,
collectedAt: OffsetDateTime?,
Expand All @@ -136,21 +136,12 @@ class TraktEpisodeSync(
}
val lastCollectedAt = TraktSettings.getLastEpisodesCollectedAt(context)
if (isInitialSync || TimeTools.isAfterMillis(collectedAt, lastCollectedAt)) {
val collectedShowsTrakt = try {
val response = traktSync!!.sync
.collectionShows(null)
.execute()
if (!response.isSuccessful) {
if (SgTrakt.isUnauthorized(context, response)) {
return false
}
Errors.logAndReport("get collected shows", response)
return false

val collectedShowsTrakt = runBlocking(Dispatchers.Default) {
when (val response = TraktTools4.getCollectedShowsByTmdbId(traktSync!!.sync)) {
is Success -> response.data
else -> null
}
response.body()
} catch (e: Exception) {
Errors.logAndReport("get collected shows", e)
return false
} ?: return false

// apply database updates, if initial sync upload diff
Expand Down Expand Up @@ -183,13 +174,11 @@ class TraktEpisodeSync(
}

private fun processTraktShows(
remoteShows: List<BaseShow>,
tmdbIdsToTraktShow: Map<Int, BaseShow>,
tmdbIdsToShowIds: Map<Int, Long>,
flag: Flag,
isInitialSync: Boolean
): Boolean {
val tmdbIdsToTraktShow = TraktTools2.mapByTmdbId(remoteShows)

var uploadedShowsCount = 0
val showIdsToLastWatched: MutableMap<Long, Long> = HashMap()
val showsToClear = ArrayList<Long>()
Expand Down
13 changes: 9 additions & 4 deletions app/src/main/java/com/battlelancer/seriesguide/sync/TraktSync.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2017-2025 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2017 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.sync

Expand Down Expand Up @@ -49,7 +49,8 @@ class TraktSync(
* To not conflict with Hexagon sync, can turn on [onlyRatings] so only
* ratings are synced.
*
* Note: this calls [TraktNotesSync.syncForShows] which may throw [InterruptedException].
* Note: this calls [syncEpisodes] and [TraktNotesSync.syncForShows] which may throw
* [InterruptedException].
*/
@Throws(InterruptedException::class)
fun sync(onlyRatings: Boolean): SgSyncAdapter.UpdateResult {
Expand Down Expand Up @@ -136,8 +137,12 @@ class TraktSync(
/**
* Downloads and uploads episode watched and collected flags.
*
* Do **NOT** call if there are no local shows to avoid unnecessary work.
* Do **NOT** call if there are no local shows to avoid unnecessary work.
*
* Note: this calls [TraktEpisodeSync.syncWatched] and [TraktEpisodeSync.syncCollected] which
* may throw [InterruptedException].
*/
@Throws(InterruptedException::class)
private fun syncEpisodes(
tmdbIdsToShowIds: Map<Int, Long>,
lastActivity: LastActivityMore
Expand Down Expand Up @@ -170,7 +175,7 @@ class TraktSync(
fun <T> handleUnsuccessfulResponse(response: Response<T>, action: String) {
if (SgTrakt.isUnauthorized(context, response)) {
return // Do not report auth errors.
} else if (SgTrakt.isAccountLimitExceeded(response)) {
} else if (TraktV2.isAccountLimitExceeded(response)) {
// Currently should only occur on initial sync when uploading items to watchlist or
// collection (notes upload has its own error handling).
progress.setImportantErrorIfNone(context.getString(R.string.trakt_error_limit_exceeded_upload))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2014-2025 Uwe Trottmann
// SPDX-FileCopyrightText: Copyright © 2014 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.traktapi

Expand Down Expand Up @@ -90,30 +90,6 @@ class SgTrakt(
}
}

/**
* Returns if the response code is 420, which indicates an account limit would be exceeded.
* These limits [can be higher for VIP users](https://trakt.docs.apiary.io/#introduction/vip-methods).
*/
fun isAccountLimitExceeded(response: Response<*>): Boolean {
return response.code() == 420
}

/**
* Returns if the response code is 429, which indicates the rate limit was exceeded.
*
* [Trakt rate limiting info](https://trakt.docs.apiary.io/#introduction/rate-limiting)
*/
fun isRateLimitExceeded(response: Response<*>): Boolean {
return response.code() == 429
}

/**
* Returns if the response code is 500 or greater, which indicates a server error.
*/
fun isServerError(response: Response<*>): Boolean {
return response.code() >= 500
}

fun checkForTraktError(trakt: TraktV2, response: Response<*>): String? {
val error = trakt.checkForTraktError(response)
return if (error?.message != null) {
Expand Down
Loading