Initial adding of Kitsu scrobbler

This commit is contained in:
Zakhar Timoshenko
2023-05-03 13:54:19 +03:00
parent 78f417ebe1
commit d5c24cd5c8
11 changed files with 216 additions and 3 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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<KitsuRepository>,
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
TODO("Not yet implemented")
}
}

View File

@@ -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())
}
}

View File

@@ -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<ScrobblerManga> {
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")
}
}

View File

@@ -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,
)
}
}

View File

@@ -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)

View File

@@ -13,6 +13,8 @@
<string name="anilist_clientId" translatable="false">9887</string>
<string name="anilist_clientSecret" translatable="false">wrMqFosItQWsmB8dtAHfIFPDt15FfQi2ZGiKkJoW</string>
<string name="mal_clientId" translatable="false">6cd8e6349e9a36bc1fc1ab97703c9fd1</string>
<string name="kitsu_clientId" translatable="false">dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd</string>
<string name="kitsu_clientSecret" translatable="false">54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151</string>
<string name="acra_login" translatable="false">SxhkCVnqVLbGogvi</string>
<string name="acra_password" translatable="false">xPDACTLHnHU9Nfjv</string>
<string name="sync_authority_history" translatable="false">org.koitharu.kotatsu.history</string>

View File

@@ -435,4 +435,5 @@
<string name="show_on_shelf">Show on the Shelf</string>
<string name="sync_auth_hint">You can sign in into an existing account or create a new one</string>
<string name="find_similar">Find similar</string>
<string name="kitsu" translatable="false">Kitsu</string>
</resources>