Phase 4: domain layer — ServerSettings, RemoteLibrary model, repository interface + impl
This commit is contained in:
parent
33fddc922b
commit
2a6b8a219d
5 changed files with 176 additions and 0 deletions
|
|
@ -34,12 +34,14 @@ import org.dueattendant149.bookshelf.data.repository.ColorPresetRepositoryImpl
|
||||||
import org.dueattendant149.bookshelf.data.repository.FileSystemRepositoryImpl
|
import org.dueattendant149.bookshelf.data.repository.FileSystemRepositoryImpl
|
||||||
import org.dueattendant149.bookshelf.data.repository.HistoryRepositoryImpl
|
import org.dueattendant149.bookshelf.data.repository.HistoryRepositoryImpl
|
||||||
import org.dueattendant149.bookshelf.data.repository.PermissionRepositoryImpl
|
import org.dueattendant149.bookshelf.data.repository.PermissionRepositoryImpl
|
||||||
|
import org.dueattendant149.bookshelf.data.repository.RemoteLibraryRepositoryImpl
|
||||||
import org.dueattendant149.bookshelf.domain.repository.BookRepository
|
import org.dueattendant149.bookshelf.domain.repository.BookRepository
|
||||||
import org.dueattendant149.bookshelf.domain.repository.CategoryRepository
|
import org.dueattendant149.bookshelf.domain.repository.CategoryRepository
|
||||||
import org.dueattendant149.bookshelf.domain.repository.ColorPresetRepository
|
import org.dueattendant149.bookshelf.domain.repository.ColorPresetRepository
|
||||||
import org.dueattendant149.bookshelf.domain.repository.FileSystemRepository
|
import org.dueattendant149.bookshelf.domain.repository.FileSystemRepository
|
||||||
import org.dueattendant149.bookshelf.domain.repository.HistoryRepository
|
import org.dueattendant149.bookshelf.domain.repository.HistoryRepository
|
||||||
import org.dueattendant149.bookshelf.domain.repository.PermissionRepository
|
import org.dueattendant149.bookshelf.domain.repository.PermissionRepository
|
||||||
|
import org.dueattendant149.bookshelf.domain.repository.RemoteLibraryRepository
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
@ -87,6 +89,12 @@ abstract class RepositoryModule {
|
||||||
categoryRepositoryImpl: CategoryRepositoryImpl
|
categoryRepositoryImpl: CategoryRepositoryImpl
|
||||||
): CategoryRepository
|
): CategoryRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindRemoteLibraryRepository(
|
||||||
|
remoteLibraryRepositoryImpl: RemoteLibraryRepositoryImpl
|
||||||
|
): RemoteLibraryRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindBookMapper(
|
abstract fun bindBookMapper(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.data.repository
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import org.dueattendant149.bookshelf.core.ui.UIText
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.BookshelfApiClientFactory
|
||||||
|
import org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.LibraryResponse
|
||||||
|
import org.dueattendant149.bookshelf.data.settings.ServerSettings
|
||||||
|
import org.dueattendant149.bookshelf.domain.model.library.Book
|
||||||
|
import org.dueattendant149.bookshelf.domain.model.remote.RemoteLibrary
|
||||||
|
import org.dueattendant149.bookshelf.domain.repository.RemoteLibraryRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class RemoteLibraryRepositoryImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val clientFactory: BookshelfApiClientFactory,
|
||||||
|
private val serverSettings: ServerSettings,
|
||||||
|
) : RemoteLibraryRepository {
|
||||||
|
override suspend fun getLibraries(): Result<List<RemoteLibrary>> {
|
||||||
|
return runCatching {
|
||||||
|
val client = client() ?: return Result.success(emptyList())
|
||||||
|
val response = client.getLibraries()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
return Result.failure(RuntimeException("Failed to fetch libraries: ${response.code()}"))
|
||||||
|
}
|
||||||
|
response.body().orEmpty().map { it.toDomain() }
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("RemoteLibraryRepo", "getLibraries failed", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getBooks(libraryId: String): Result<List<Book>> {
|
||||||
|
return searchLibrary(libraryId, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun searchLibrary(
|
||||||
|
libraryId: String,
|
||||||
|
query: String,
|
||||||
|
): Result<List<Book>> {
|
||||||
|
return runCatching {
|
||||||
|
val client = client() ?: return Result.success(emptyList())
|
||||||
|
val response = client.searchLibrary(libraryId, query)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
return Result.failure(RuntimeException("Failed to search library: ${response.code()}"))
|
||||||
|
}
|
||||||
|
response.body().orEmpty().map { it.toBook() }
|
||||||
|
}.onFailure {
|
||||||
|
Log.e("RemoteLibraryRepo", "searchLibrary failed", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun client() = serverSettings.getBookshelfUrl()?.let { clientFactory.provideClient(it) }
|
||||||
|
|
||||||
|
private fun LibraryResponse.toDomain() =
|
||||||
|
RemoteLibrary(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
mediaType = mediaType,
|
||||||
|
itemCount = itemCount,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun org.dueattendant149.bookshelf.data.remote.bookshelfapi.model.BookItemResponse.toBook() =
|
||||||
|
Book(
|
||||||
|
id = 0,
|
||||||
|
title = title,
|
||||||
|
author = UIText.StringValue(author),
|
||||||
|
description = null,
|
||||||
|
filePath = "",
|
||||||
|
coverImage = null,
|
||||||
|
scrollIndex = 0,
|
||||||
|
scrollOffset = 0,
|
||||||
|
progress = 0f,
|
||||||
|
lastOpened = null,
|
||||||
|
categories = emptyList(),
|
||||||
|
remoteId = id,
|
||||||
|
libraryId = libraryId,
|
||||||
|
mediaType = mediaType,
|
||||||
|
hasAudio = mediaType == "audiobook" || mediaType == "podcast",
|
||||||
|
hasEbook = mediaType == "book",
|
||||||
|
audioDuration = duration.toLong(),
|
||||||
|
coverUrl = coverUrl,
|
||||||
|
lastSyncedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.data.settings
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import org.dueattendant149.bookshelf.data.local.data_store.DataStore
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ServerSettings
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val dataStore: DataStore,
|
||||||
|
) {
|
||||||
|
private val bookshelfUrlKey = stringPreferencesKey("bookshelf_url")
|
||||||
|
private val absUrlKey = stringPreferencesKey("abs_url")
|
||||||
|
private val absTokenKey = stringPreferencesKey("abs_token")
|
||||||
|
|
||||||
|
suspend fun getBookshelfUrl(): String? = dataStore.getNullableData(bookshelfUrlKey)
|
||||||
|
suspend fun setBookshelfUrl(value: String) = dataStore.putData(bookshelfUrlKey, value)
|
||||||
|
|
||||||
|
suspend fun getAbsUrl(): String? = dataStore.getNullableData(absUrlKey)
|
||||||
|
suspend fun setAbsUrl(value: String) = dataStore.putData(absUrlKey, value)
|
||||||
|
|
||||||
|
suspend fun getAbsToken(): String? = dataStore.getNullableData(absTokenKey)
|
||||||
|
suspend fun setAbsToken(value: String) = dataStore.putData(absTokenKey, value)
|
||||||
|
|
||||||
|
suspend fun hasCredentials(): Boolean =
|
||||||
|
!getBookshelfUrl().isNullOrBlank() &&
|
||||||
|
!getAbsUrl().isNullOrBlank() &&
|
||||||
|
!getAbsToken().isNullOrBlank()
|
||||||
|
|
||||||
|
suspend fun clear() {
|
||||||
|
setBookshelfUrl("")
|
||||||
|
setAbsUrl("")
|
||||||
|
setAbsToken("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.domain.model.remote
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audiobookshelf library exposed through bookshelf-api.
|
||||||
|
*/
|
||||||
|
data class RemoteLibrary(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val mediaType: String,
|
||||||
|
val itemCount: Int,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.domain.repository
|
||||||
|
|
||||||
|
import org.dueattendant149.bookshelf.domain.model.library.Book
|
||||||
|
import org.dueattendant149.bookshelf.domain.model.remote.RemoteLibrary
|
||||||
|
|
||||||
|
interface RemoteLibraryRepository {
|
||||||
|
suspend fun getLibraries(): Result<List<RemoteLibrary>>
|
||||||
|
suspend fun getBooks(libraryId: String): Result<List<Book>>
|
||||||
|
suspend fun searchLibrary(libraryId: String, query: String): Result<List<Book>>
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue