Skip to content

Commit f72609b

Browse files
Surjit Kumar SahooSurjit Kumar Sahoo
authored andcommitted
feat: enhance book caching and MiniPlayer flexibility, and improve accessibility
1 parent 428b273 commit f72609b

File tree

26 files changed

+311
-145
lines changed

26 files changed

+311
-145
lines changed

.agent/rules/project-context.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This is an offline first Android client app for the [audiobookshelf](https://git
1515
- The app should watch the network changes, such as connecting to a new wifi network, or lan network, or disconnecting from a network and connecting to a new network, disconnecting from wifi and connecting to celular network etc, and try to ping or reachout the audiobook server to check whether the server is reachable.
1616
- If the server is reachable, the app should sync the local progress to the server and pull the latest progress updates from the server to the local database, it should merge the updates from both.
1717
- If the server is reachable, and some chapters of any audiobook is download, i.e. available offline, then offline track should be given priority for playback.
18-
- If the offline track is deleted / cleared while the book is being played, it should fallback to the online URL if the server is reachable, else it should pause the playback, and make sure to correctly store the last
18+
- If the offline track is deleted / cleared while the book is being played, the player should attempt to fallback to the online URL if the server is reachable, otherwise pause playback and persist the last playback state (track ID, playback position/timestamp, current chapter/index, and playback status) plus a flag indicating offline content was removed; ensure the rule notes that these persisted fields are used to resume or report playback state and are written atomically to the player state store.
1919
- The app must be fully functional for downloaded content when offline.
2020

2121
## Ensure Stability
@@ -28,7 +28,7 @@ This is an offline first Android client app for the [audiobookshelf](https://git
2828
- When the app loads, it should load the offline available book content immediately, then in the background reach out to the server (if the server is reachable) and fetch the full list of the books, continue listening section books and update the UI seemlessly.
2929
- The app should cache all the book's metadata in local database to optimise the app load time, Only the chapters / audio tracks should not be cached automatically / by default. Chapters should be downloaded and cached on demand by the user, using the download chapters/book functionality.
3030
- When the app loads, if the server is not reachable, it shouldn't show long loading screen, trying to fetch the books from the server, it should load the book's list from the local database, however it should only show the books whos' chapters are downloaded and available offline can be played.
31-
- When the server becomes reachable, it should update the books list, as now all the books can be played from local cahce or online from the server.
31+
- When the server becomes reachable, it should update the books list, as now all the books can be played from local cache or online from the server.
3232
- When the network is switched, the app should trigger checking whether the server is still reachable or not, if not reachable, it should update the UI to only show offline available ready to play books.
3333

3434
## General Guidelines and Standards

app/src/main/kotlin/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastPageResponseConverter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class PodcastPageResponseConverter
2525
subtitle = null,
2626
series = null,
2727
author = it.media.metadata.author,
28+
// Duration is unavailable from the API
2829
duration = 0.0,
2930
libraryId = libraryId,
3031
)

app/src/main/kotlin/org/grakovne/lissen/common/NetworkService.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope
1111
import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.Job
1313
import kotlinx.coroutines.SupervisorJob
14+
import kotlinx.coroutines.cancel
1415
import kotlinx.coroutines.delay
1516
import kotlinx.coroutines.flow.MutableStateFlow
1617
import kotlinx.coroutines.flow.StateFlow
@@ -92,18 +93,19 @@ class NetworkService
9293

9394
val hostUrl = preferences.getHost()
9495
if (hostUrl.isNullOrBlank()) {
95-
_isServerAvailable.emit(isConnectedToInternet)
96+
_isServerAvailable.emit(false)
9697
return@launch
9798
}
9899

99100
try {
100101
val url = java.net.URL(hostUrl)
101102
val port = if (url.port == -1) url.defaultPort else url.port
102-
val socket = java.net.Socket()
103103
val address = java.net.InetSocketAddress(url.host, port)
104104

105-
socket.connect(address, 2000)
106-
socket.close()
105+
java.net.Socket().use { socket ->
106+
socket.connect(address, 2000)
107+
}
108+
107109
_isServerAvailable.emit(true)
108110
} catch (e: Exception) {
109111
Timber.e(e, "Server reachability check failed for $hostUrl")
@@ -156,4 +158,8 @@ class NetworkService
156158
cachedNetworkHandle = network.networkHandle
157159
return cachedSsid
158160
}
161+
162+
override fun onDestroy() {
163+
scope.cancel()
164+
}
159165
}

app/src/main/kotlin/org/grakovne/lissen/common/RunningComponent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ package org.grakovne.lissen.common
22

33
interface RunningComponent {
44
fun onCreate()
5+
6+
fun onDestroy() {}
57
}

app/src/main/kotlin/org/grakovne/lissen/content/AuthRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class AuthRepository
2525
username: String,
2626
password: String,
2727
): OperationResult<UserAccount> {
28-
Timber.d("Authorizing for $username@$host")
28+
Timber.d("Authorizing for host: $host")
2929
return provideAuthService().authorize(host, username, password) { onPostLogin(host, it) }
3030
}
3131

app/src/main/kotlin/org/grakovne/lissen/content/BookRepository.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow
55
import kotlinx.coroutines.flow.combine
66
import org.grakovne.lissen.channel.audiobookshelf.AudiobookshelfChannelProvider
77
import org.grakovne.lissen.channel.common.MediaChannel
8+
import org.grakovne.lissen.channel.common.OperationError
89
import org.grakovne.lissen.channel.common.OperationResult
910
import org.grakovne.lissen.common.NetworkService
1011
import org.grakovne.lissen.content.cache.persistent.LocalCacheRepository
@@ -46,12 +47,20 @@ class BookRepository
4647
}
4748

4849
Timber.d("Local URI miss for $libraryItemId / $chapterId. Falling back to REMOTE.")
49-
return providePreferredChannel()
50-
.provideFileUri(libraryItemId, chapterId)
51-
.let {
52-
Timber.d("Providing REMOTE URI for $libraryItemId / $chapterId: $it")
53-
OperationResult.Success(it)
50+
51+
return try {
52+
val uri = providePreferredChannel().provideFileUri(libraryItemId, chapterId)
53+
54+
if (uri == null) {
55+
OperationResult.Error(OperationError.InternalError, "Remote URI is null")
56+
} else {
57+
Timber.d("Providing REMOTE URI for $libraryItemId / $chapterId: $uri")
58+
OperationResult.Success(uri)
5459
}
60+
} catch (e: Exception) {
61+
Timber.e(e, "Failed to provide file URI for $libraryItemId and $chapterId")
62+
OperationResult.Error(OperationError.InternalError, e.message ?: "Unknown error occurred")
63+
}
5564
}
5665

5766
suspend fun syncProgress(

app/src/main/kotlin/org/grakovne/lissen/content/LissenMediaProvider.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ class LissenMediaProvider
5050
}
5151

5252
Timber.d("Local URI miss for $libraryItemId / $chapterId. Falling back to REMOTE.")
53-
return providePreferredChannel()
54-
.provideFileUri(libraryItemId, chapterId)
55-
.let {
56-
Timber.d("Providing REMOTE URI for $libraryItemId / $chapterId: $it")
57-
OperationResult.Success(it)
58-
}
53+
return try {
54+
val uri = providePreferredChannel().provideFileUri(libraryItemId, chapterId)
55+
Timber.d("Providing REMOTE URI for $libraryItemId / $chapterId: $uri")
56+
OperationResult.Success(uri)
57+
} catch (e: Exception) {
58+
Timber.e(e, "Failed to provide file URI for $libraryItemId and $chapterId")
59+
OperationResult.Error(OperationError.InternalError, e.message)
60+
}
5961
}
6062

6163
suspend fun syncProgress(

app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/ContentCachingManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class ContentCachingManager
110110

111111
findRequestedFiles(item, listOf(chapter))
112112
.forEach { file ->
113-
val binaryContent = properties.provideMediaCachePatch(item.id, file.id)
113+
val binaryContent = properties.provideMediaCachePath(item.id, file.id)
114114

115115
if (binaryContent.exists()) {
116116
binaryContent.delete()
@@ -171,7 +171,7 @@ class ContentCachingManager
171171
}
172172

173173
val body = response.body
174-
val dest = properties.provideMediaCachePatch(bookId, file.id)
174+
val dest = properties.provideMediaCachePath(bookId, file.id)
175175
dest.parentFile?.mkdirs()
176176

177177
try {

app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/LocalCacheRepository.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class LocalCacheRepository
121121

122122
suspend fun fetchRecentListenedBooks(
123123
libraryId: String,
124-
downloadedOnly: Boolean,
124+
downloadedOnly: Boolean = false,
125125
): OperationResult<List<RecentBook>> =
126126
cachedBookRepository
127127
.fetchRecentBooks(
@@ -161,24 +161,24 @@ class LocalCacheRepository
161161

162162
suspend fun cacheBookMetadata(book: DetailedItem) {
163163
try {
164-
val restoredChapters =
164+
val (restoredChapters, droppedChapters) =
165165
book
166166
.chapters
167-
.filter { chapter ->
167+
.partition { chapter ->
168168
val files = findRelatedFiles(chapter, book.files)
169-
if (files.isEmpty()) return@filter false
169+
if (files.isEmpty()) return@partition false
170170

171171
files.all { file ->
172172
storageProperties
173-
.provideMediaCachePatch(book.id, file.id)
173+
.provideMediaCachePath(book.id, file.id)
174174
.exists()
175175
}
176176
}
177177

178178
cachedBookRepository.cacheBook(
179179
book = book,
180180
fetchedChapters = restoredChapters,
181-
droppedChapters = emptyList(),
181+
droppedChapters = droppedChapters,
182182
)
183183
Timber.d("Successfully cached book metadata for ${book.id}")
184184
} catch (e: Exception) {

app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/OfflineBookStorageProperties.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class OfflineBookStorageProperties
2727

2828
fun provideBookCache(bookId: String): File = baseFolder().resolve(bookId)
2929

30-
fun provideMediaCachePatch(
30+
fun provideMediaCachePath(
3131
bookId: String,
3232
fileId: String,
3333
): File =

0 commit comments

Comments
 (0)