Phase 4: domain layer — ServerSettings, RemoteLibrary model, repository interface + impl

This commit is contained in:
Atte149 2026-06-15 21:33:13 +03:00
parent 33fddc922b
commit 2a6b8a219d
5 changed files with 176 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

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