diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/di/RepositoryModule.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/di/RepositoryModule.kt index 08b1cf60..83db147b 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/di/RepositoryModule.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/di/RepositoryModule.kt @@ -34,12 +34,14 @@ import org.dueattendant149.bookshelf.data.repository.ColorPresetRepositoryImpl import org.dueattendant149.bookshelf.data.repository.FileSystemRepositoryImpl import org.dueattendant149.bookshelf.data.repository.HistoryRepositoryImpl 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.CategoryRepository import org.dueattendant149.bookshelf.domain.repository.ColorPresetRepository import org.dueattendant149.bookshelf.domain.repository.FileSystemRepository import org.dueattendant149.bookshelf.domain.repository.HistoryRepository import org.dueattendant149.bookshelf.domain.repository.PermissionRepository +import org.dueattendant149.bookshelf.domain.repository.RemoteLibraryRepository import javax.inject.Singleton @Module @@ -87,6 +89,12 @@ abstract class RepositoryModule { categoryRepositoryImpl: CategoryRepositoryImpl ): CategoryRepository + @Binds + @Singleton + abstract fun bindRemoteLibraryRepository( + remoteLibraryRepositoryImpl: RemoteLibraryRepositoryImpl + ): RemoteLibraryRepository + @Binds @Singleton abstract fun bindBookMapper( diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/repository/RemoteLibraryRepositoryImpl.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/repository/RemoteLibraryRepositoryImpl.kt new file mode 100644 index 00000000..25ac31e4 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/repository/RemoteLibraryRepositoryImpl.kt @@ -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> { + 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> { + return searchLibrary(libraryId, "") + } + + override suspend fun searchLibrary( + libraryId: String, + query: String, + ): Result> { + 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(), + ) + } diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/settings/ServerSettings.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/settings/ServerSettings.kt new file mode 100644 index 00000000..d809090d --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/settings/ServerSettings.kt @@ -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("") + } + } diff --git a/app/src/main/java/org/dueattendant149/bookshelf/domain/model/remote/RemoteLibrary.kt b/app/src/main/java/org/dueattendant149/bookshelf/domain/model/remote/RemoteLibrary.kt new file mode 100644 index 00000000..4dd8442f --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/domain/model/remote/RemoteLibrary.kt @@ -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, +) diff --git a/app/src/main/java/org/dueattendant149/bookshelf/domain/repository/RemoteLibraryRepository.kt b/app/src/main/java/org/dueattendant149/bookshelf/domain/repository/RemoteLibraryRepository.kt new file mode 100644 index 00000000..7c9b8f7d --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/domain/repository/RemoteLibraryRepository.kt @@ -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> + suspend fun getBooks(libraryId: String): Result> + suspend fun searchLibrary(libraryId: String, query: String): Result> +}