From d5c24cd5c8b1b7b0120ae6e7178613d22fdeaf14 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Wed, 3 May 2023 13:54:19 +0300 Subject: [PATCH 1/4] Initial adding of Kitsu scrobbler --- README.md | 2 +- .../kotatsu/core/prefs/AppSettings.kt | 1 + .../kotatsu/scrobbling/ScrobblingModule.kt | 32 +++++++- .../common/domain/model/ScrobblerService.kt | 3 +- .../kitsu/data/KitsuAuthenticator.kt | 22 ++++++ .../scrobbling/kitsu/data/KitsuInterceptor.kt | 25 ++++++ .../scrobbling/kitsu/data/KitsuRepository.kt | 77 +++++++++++++++++++ .../scrobbling/kitsu/domain/KitsuScrobbler.kt | 40 ++++++++++ .../settings/ServicesSettingsFragment.kt | 14 ++++ app/src/main/res/values/constants.xml | 2 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt diff --git a/README.md b/README.md index 759d4298d..0b4211632 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Kotatsu is a free and open source manga reader for Android. * Tablet-optimized Material You UI * Standard and Webtoon-optimized reader * Notifications about new chapters with updates feed -* Integration with manga tracking services: Shikimori, AniList, MyAnimeList +* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu * Password/fingerprint protect access to the app * History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 6386dff7c..471c3eece 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -381,6 +381,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SHIKIMORI = "shikimori" const val KEY_ANILIST = "anilist" const val KEY_MAL = "mal" + const val KEY_KITSU = "kitsu" const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism" const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt index f4300792b..607cbc5e1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt @@ -19,6 +19,10 @@ import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType +import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuAuthenticator +import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuInterceptor +import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository +import org.koitharu.kotatsu.scrobbling.kitsu.domain.KitsuScrobbler import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository @@ -87,6 +91,24 @@ object ScrobblingModule { return AniListRepository(context, okHttp, storage, database) } + @Provides + @Singleton + fun provideKitsuRepository( + @ApplicationContext context: Context, + @ScrobblerType(ScrobblerService.KITSU) storage: ScrobblerStorage, + database: MangaDatabase, + authenticator: KitsuAuthenticator, + ): KitsuRepository { + val okHttp = OkHttpClient.Builder().apply { + authenticator(authenticator) + addInterceptor(KitsuInterceptor(storage)) + if (BuildConfig.DEBUG) { + addInterceptor(CurlLoggingInterceptor()) + } + }.build() + return KitsuRepository(context, okHttp, storage, database) + } + @Provides @Singleton @ScrobblerType(ScrobblerService.ANILIST) @@ -108,11 +130,19 @@ object ScrobblingModule { @ApplicationContext context: Context, ): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.MAL) + @Provides + @Singleton + @ScrobblerType(ScrobblerService.KITSU) + fun provideKitsuStorage( + @ApplicationContext context: Context, + ): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.KITSU) + @Provides @ElementsIntoSet fun provideScrobblers( shikimoriScrobbler: ShikimoriScrobbler, aniListScrobbler: AniListScrobbler, malScrobbler: MALScrobbler, - ): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler) + kitsuScrobbler: KitsuScrobbler + ): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler, kitsuScrobbler) } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt index 5b47a3a25..8760e8073 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt @@ -12,5 +12,6 @@ enum class ScrobblerService( SHIKIMORI(1, R.string.shikimori, R.drawable.ic_shikimori), ANILIST(2, R.string.anilist, R.drawable.ic_anilist), - MAL(3, R.string.mal, R.drawable.ic_mal) + MAL(3, R.string.mal, R.drawable.ic_mal), + KITSU(4, R.string.kitsu, R.drawable.ic_script) } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt new file mode 100644 index 000000000..106651267 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.scrobbling.kitsu.data + +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType +import javax.inject.Inject +import javax.inject.Provider + +class KitsuAuthenticator @Inject constructor( + @ScrobblerType(ScrobblerService.KITSU) private val storage: ScrobblerStorage, + private val repositoryProvider: Provider, +) : Authenticator { + + override fun authenticate(route: Route?, response: Response): Request? { + TODO("Not yet implemented") + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt new file mode 100644 index 000000000..13b0069e3 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.scrobbling.kitsu.data + +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage + +private const val JSON = "application/json" + +class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val sourceRequest = chain.request() + val request = sourceRequest.newBuilder() + request.header(CommonHeaders.CONTENT_TYPE, JSON) + request.header(CommonHeaders.ACCEPT, JSON) + if (!sourceRequest.url.pathSegments.contains("oauth")) { + storage.accessToken?.let { + request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") + } + } + return chain.proceed(request.build()) + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt new file mode 100644 index 000000000..93d669989 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.scrobbling.kitsu.data + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.OkHttpClient +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository +import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser + +private const val BASE_WEB_URL = "https://kitsu.io" + +class KitsuRepository( + @ApplicationContext context: Context, + private val okHttp: OkHttpClient, + private val storage: ScrobblerStorage, + private val db: MangaDatabase, +) : ScrobblerRepository { + + private val clientId = context.getString(R.string.kitsu_clientId) + private val clientSecret = context.getString(R.string.kitsu_clientSecret) + + override val oauthUrl: String + get() = "${BASE_WEB_URL}/api/oauth2/token" + + "?username=..." + // Get from AlertDialog... + "&password=..." + // Get from AlertDialog... + "&grant_type=password" + + "&client_id=$clientId" + + "&client_secret=$clientSecret" + + override val isAuthorized: Boolean + get() = TODO("Not yet implemented") + + override val cachedUser: ScrobblerUser? + get() = TODO("Not yet implemented") + + override suspend fun authorize(code: String?) { + TODO("Not yet implemented") + } + + override suspend fun loadUser(): ScrobblerUser { + TODO("Not yet implemented") + } + + override fun logout() { + TODO("Not yet implemented") + } + + override suspend fun unregister(mangaId: Long) { + TODO("Not yet implemented") + } + + override suspend fun findManga(query: String, offset: Int): List { + TODO("Not yet implemented") + } + + override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo { + TODO("Not yet implemented") + } + + override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) { + TODO("Not yet implemented") + } + + override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) { + TODO("Not yet implemented") + } + + override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) { + TODO("Not yet implemented") + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt new file mode 100644 index 000000000..8321dae01 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/domain/KitsuScrobbler.kt @@ -0,0 +1,40 @@ +package org.koitharu.kotatsu.scrobbling.kitsu.domain + +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus +import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository +import javax.inject.Inject + +class KitsuScrobbler @Inject constructor( + private val repository: KitsuRepository, + db: MangaDatabase, +) : Scrobbler(db, ScrobblerService.KITSU, repository) { + + init { + statuses[ScrobblingStatus.PLANNED] = "planned" + statuses[ScrobblingStatus.READING] = "current" + statuses[ScrobblingStatus.COMPLETED] = "completed" + statuses[ScrobblingStatus.ON_HOLD] = "on_hold" + statuses[ScrobblingStatus.DROPPED] = "dropped" + } + + override suspend fun updateScrobblingInfo( + mangaId: Long, + rating: Float, + status: ScrobblingStatus?, + comment: String? + ) { + val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) + requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" } + repository.updateRate( + rateId = entity.id, + mangaId = entity.mangaId, + rating = rating, + status = statuses[status], + comment = comment, + ) + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt index 2a4561ff3..9f63f32bb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity +import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.sync.domain.SyncController @@ -38,6 +39,9 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { @Inject lateinit var malRepository: MALRepository + @Inject + lateinit var kitsuRepository: KitsuRepository + @Inject lateinit var syncController: SyncController @@ -50,6 +54,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { bindScrobblerSummary(AppSettings.KEY_SHIKIMORI, shikimoriRepository) bindScrobblerSummary(AppSettings.KEY_ANILIST, aniListRepository) bindScrobblerSummary(AppSettings.KEY_MAL, malRepository) + bindScrobblerSummary(AppSettings.KEY_KITSU, kitsuRepository) bindSyncSummary() } @@ -82,6 +87,15 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { true } + AppSettings.KEY_KITSU -> { + if (!kitsuRepository.isAuthorized) { + launchScrobblerAuth(kitsuRepository) + } else { + startActivity(ScrobblerConfigActivity.newIntent(preference.context, ScrobblerService.KITSU)) + } + true + } + AppSettings.KEY_SYNC -> { val am = AccountManager.get(requireContext()) val accountType = getString(R.string.account_type_sync) diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 324fc1d75..6ab464d4c 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -13,6 +13,8 @@ 9887 wrMqFosItQWsmB8dtAHfIFPDt15FfQi2ZGiKkJoW 6cd8e6349e9a36bc1fc1ab97703c9fd1 + dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd + 54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151 SxhkCVnqVLbGogvi xPDACTLHnHU9Nfjv org.koitharu.kotatsu.history diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 148c22c77..296213693 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,4 +435,5 @@ Show on the Shelf You can sign in into an existing account or create a new one Find similar + Kitsu From 41551451b0cb588c8af11a7194bfdc6d5a875530 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 6 May 2023 16:15:17 +0300 Subject: [PATCH 2/4] Part 1 --- app/src/main/AndroidManifest.xml | 4 + .../common/domain/model/ScrobblerService.kt | 2 +- .../scrobbling/kitsu/data/KitsuRepository.kt | 37 +++-- .../scrobbling/kitsu/ui/KitsuAuthActivity.kt | 32 +++++ .../settings/ServicesSettingsFragment.kt | 4 + app/src/main/res/drawable/ic_kitsu.xml | 10 ++ .../main/res/layout/activity_kitsu_auth.xml | 130 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_services.xml | 15 +- 9 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt create mode 100644 app/src/main/res/drawable/ic_kitsu.xml create mode 100644 app/src/main/res/layout/activity_kitsu_auth.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f70692fb5..c2f926d41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -161,6 +161,10 @@ + { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt new file mode 100644 index 000000000..55ae9d8ff --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.scrobbling.kitsu.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.graphics.Insets +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding + +class KitsuAuthActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater)) + } + + override fun onWindowInsetsChanged(insets: Insets) { + val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding) + binding.root.setPadding( + basePadding + insets.left, + basePadding + insets.top, + basePadding + insets.right, + basePadding + insets.bottom, + ) + } + + companion object { + fun newIntent(context: Context) = Intent(context, KitsuAuthActivity::class.java) + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt index 9f63f32bb..6fb976e28 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt @@ -18,6 +18,7 @@ import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository +import org.koitharu.kotatsu.scrobbling.kitsu.ui.KitsuAuthActivity import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.sync.domain.SyncController @@ -146,6 +147,9 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { private fun launchScrobblerAuth(repository: org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository) { runCatching { + if (repository.oauthUrl.isBlank()) { + startActivity(KitsuAuthActivity.newIntent(requireContext())) + } val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(repository.oauthUrl) startActivity(intent) diff --git a/app/src/main/res/drawable/ic_kitsu.xml b/app/src/main/res/drawable/ic_kitsu.xml new file mode 100644 index 000000000..b124346cb --- /dev/null +++ b/app/src/main/res/drawable/ic_kitsu.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_kitsu_auth.xml b/app/src/main/res/layout/activity_kitsu_auth.xml new file mode 100644 index 000000000..f6166696e --- /dev/null +++ b/app/src/main/res/layout/activity_kitsu_auth.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + +