feat(backend): add thin BookshelfApiRepository and AudiobookshelfRepository
- BookshelfApiRepository exposes all bookshelf-api endpoints as Result<T>. - AudiobookshelfRepository exposes direct ABS audio/cover/progress endpoints. - Wire both repositories into BackendModule for Hilt injection. - ':app:assembleOssDebug' still passes.
This commit is contained in:
parent
907d7c8d24
commit
a8ad2bf900
3 changed files with 246 additions and 0 deletions
|
|
@ -0,0 +1,72 @@
|
|||
package org.dueattendant149.bookreader.data.remote.audiobookshelf
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import org.dueattendant149.bookreader.data.remote.audiobookshelf.model.AbsItemResponse
|
||||
import org.dueattendant149.bookreader.data.settings.ServerSettings
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Thin repository over [AudiobookshelfApiService]. Direct ABS calls for audio,
|
||||
* cover and progress. bookshelf-api does not proxy audio streams.
|
||||
*/
|
||||
@Singleton
|
||||
class AudiobookshelfRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val clientFactory: AudiobookshelfApiClientFactory,
|
||||
private val serverSettings: ServerSettings,
|
||||
) {
|
||||
private suspend fun client() =
|
||||
serverSettings.getAbsUrl()?.let { url ->
|
||||
serverSettings.getAbsToken()?.let { token ->
|
||||
clientFactory.provideClient(url, token)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getItem(itemId: String): Result<AbsItemResponse> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.getItem(itemId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun getCover(itemId: String): Result<ResponseBody> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.getCover(itemId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun downloadBook(itemId: String): Result<ResponseBody> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.downloadBook(itemId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun downloadAudioFile(
|
||||
itemId: String,
|
||||
fileId: String,
|
||||
): Result<ResponseBody> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.downloadAudioFile(itemId, fileId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun getProgress(itemId: String): Result<String> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.getProgress(itemId).unwrapBody() ?: "" }
|
||||
}
|
||||
|
||||
suspend fun updateProgress(
|
||||
itemId: String,
|
||||
request: PlaybackProgressUpdateRequest,
|
||||
): Result<String> {
|
||||
val client = client() ?: return Result.failure(absNotConfigured())
|
||||
return safe { client.updateProgress(itemId, request).unwrapBody() ?: "" }
|
||||
}
|
||||
|
||||
private fun absNotConfigured() = IllegalStateException("Audiobookshelf URL/token not configured")
|
||||
|
||||
private inline fun <T> safe(block: () -> T): Result<T> = runCatching(block)
|
||||
|
||||
private fun <T> Response<T>.unwrapBody(): T? {
|
||||
if (!isSuccessful) throw IllegalStateException("HTTP ${code()}: ${message()}")
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package org.dueattendant149.bookreader.data.remote.bookshelfapi
|
||||
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.DownloadRequest
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.DownloadResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.DownloadsListResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.LibraryItemsResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.LibraryResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.SearchRequest
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.SearchResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.TtsEnginesResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.TtsJobResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.TtsJobsResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.TtsVoicesResponse
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.UploadBookResponse
|
||||
import org.dueattendant149.bookreader.data.settings.ServerSettings
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Thin repository over [BookshelfApiService]. Returns raw Retrofit responses
|
||||
* so callers can decide how to map them to local entities.
|
||||
*/
|
||||
@Singleton
|
||||
class BookshelfApiRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val clientFactory: BookshelfApiClientFactory,
|
||||
private val serverSettings: ServerSettings,
|
||||
) {
|
||||
private suspend fun client() = serverSettings.getBookshelfUrl()?.let { clientFactory.provideClient(it) }
|
||||
|
||||
suspend fun health(): Boolean {
|
||||
val client = client() ?: return false
|
||||
return runCatching { client.health().isSuccessful }.getOrDefault(false)
|
||||
}
|
||||
|
||||
suspend fun getLibraries(): Result<List<LibraryResponse>> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getLibraries().unwrapBody().orEmpty() }
|
||||
}
|
||||
|
||||
suspend fun getLibraryItems(libraryId: String): Result<LibraryItemsResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getLibraryItems(libraryId).unwrapBody() ?: LibraryItemsResponse() }
|
||||
}
|
||||
|
||||
suspend fun searchLibrary(
|
||||
libraryId: String,
|
||||
query: String,
|
||||
): Result<List<org.dueattendant149.bookreader.data.remote.bookshelfapi.model.BookItemResponse>> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.searchLibrary(libraryId, query).unwrapBody().orEmpty() }
|
||||
}
|
||||
|
||||
suspend fun search(request: SearchRequest): Result<SearchResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.search(request).unwrapBody() ?: SearchResponse() }
|
||||
}
|
||||
|
||||
suspend fun getBook(itemId: String): Result<okhttp3.ResponseBody> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getBook(itemId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun downloadEbook(itemId: String): Result<okhttp3.ResponseBody> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.downloadEbook(itemId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun getAudioTracks(itemId: String): Result<List<org.dueattendant149.bookreader.data.remote.bookshelfapi.model.AudioTrackResponse>> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getAudioTracks(itemId).unwrapBody().orEmpty() }
|
||||
}
|
||||
|
||||
suspend fun downloadAudioFile(
|
||||
itemId: String,
|
||||
fileId: String,
|
||||
): Result<okhttp3.ResponseBody> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.downloadAudioFile(itemId, fileId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
request: org.dueattendant149.bookreader.data.remote.bookshelfapi.model.ProgressUpdateRequest,
|
||||
): Result<Unit> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.updateReadingProgress(itemId, request).let {} }
|
||||
}
|
||||
|
||||
suspend fun updatePlaybackProgress(
|
||||
itemId: String,
|
||||
request: org.dueattendant149.bookreader.data.remote.bookshelfapi.model.PlaybackProgressUpdateRequest,
|
||||
): Result<Unit> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.updatePlaybackProgress(itemId, request).let {} }
|
||||
}
|
||||
|
||||
suspend fun getTtsEngines(): Result<TtsEnginesResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getTtsEngines().unwrapBody() ?: TtsEnginesResponse() }
|
||||
}
|
||||
|
||||
suspend fun getTtsVoices(engine: String? = null): Result<TtsVoicesResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getTtsVoices(engine).unwrapBody() ?: TtsVoicesResponse() }
|
||||
}
|
||||
|
||||
suspend fun createTtsJob(request: org.dueattendant149.bookreader.data.remote.bookshelfapi.model.TtsCreateRequest): Result<TtsJobResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.createTtsJob(request).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun getTtsJobStatus(jobId: String): Result<TtsJobResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.getTtsJobStatus(jobId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun listTtsJobs(): Result<TtsJobsResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.listTtsJobs().unwrapBody() ?: TtsJobsResponse() }
|
||||
}
|
||||
|
||||
suspend fun downloadTtsAudio(jobId: String): Result<okhttp3.ResponseBody> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.downloadTtsAudio(jobId).unwrapBody() ?: throw IllegalStateException("Empty body") }
|
||||
}
|
||||
|
||||
suspend fun startDownload(request: DownloadRequest): Result<DownloadResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.startDownload(request).unwrapBody() ?: DownloadResponse() }
|
||||
}
|
||||
|
||||
suspend fun listDownloads(): Result<DownloadsListResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.listDownloads().unwrapBody() ?: DownloadsListResponse() }
|
||||
}
|
||||
|
||||
suspend fun uploadBook(
|
||||
file: MultipartBody.Part,
|
||||
bookType: RequestBody,
|
||||
): Result<UploadBookResponse> {
|
||||
val client = client() ?: return Result.failure(serverNotConfigured())
|
||||
return safe { client.uploadBook(file, bookType).unwrapBody() ?: UploadBookResponse(success = false) }
|
||||
}
|
||||
|
||||
private fun serverNotConfigured() = IllegalStateException("Bookshelf API URL not configured")
|
||||
|
||||
private inline fun <T> safe(block: () -> T): Result<T> = runCatching(block)
|
||||
|
||||
private fun <T> Response<T>.unwrapBody(): T? {
|
||||
if (!isSuccessful) throw IllegalStateException("HTTP ${code()}: ${message()}")
|
||||
return body()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@ import dagger.hilt.components.SingletonComponent
|
|||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import org.dueattendant149.bookreader.data.remote.audiobookshelf.AudiobookshelfApiClientFactory
|
||||
import org.dueattendant149.bookreader.data.remote.audiobookshelf.AudiobookshelfRepository
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.BookshelfApiClientFactory
|
||||
import org.dueattendant149.bookreader.data.remote.bookshelfapi.BookshelfApiRepository
|
||||
import org.dueattendant149.bookreader.data.settings.ServerSettings
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -28,10 +30,24 @@ object BackendModule {
|
|||
okHttpClient: OkHttpClient,
|
||||
): BookshelfApiClientFactory = BookshelfApiClientFactory(json, okHttpClient)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBookshelfApiRepository(
|
||||
clientFactory: BookshelfApiClientFactory,
|
||||
serverSettings: ServerSettings,
|
||||
): BookshelfApiRepository = BookshelfApiRepository(clientFactory, serverSettings)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAudiobookshelfApiClientFactory(
|
||||
json: Json,
|
||||
okHttpClient: OkHttpClient,
|
||||
): AudiobookshelfApiClientFactory = AudiobookshelfApiClientFactory(json, okHttpClient)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAudiobookshelfRepository(
|
||||
clientFactory: AudiobookshelfApiClientFactory,
|
||||
serverSettings: ServerSettings,
|
||||
): AudiobookshelfRepository = AudiobookshelfRepository(clientFactory, serverSettings)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue