Fix Android Auto chapter and bookmark synchronization
Some checks failed
Build APK / main (push) Has been cancelled
Build iOS / Build iOS simulator app (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled

This commit is contained in:
FabioMich66 2026-10-20 15:26:22 +02:00
parent 4cb8cf9578
commit 867b4467bc
7 changed files with 311 additions and 31 deletions

View file

@ -3,9 +3,17 @@ package com.audiobookshelf.app.models
import com.audiobookshelf.app.data.MediaProgress
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class UserBookmark(
val libraryItemId: String,
val title: String? = null,
val time: Double
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class User(
val id: String,
val username: String,
val mediaProgress:List<MediaProgress>
val mediaProgress: List<MediaProgress> = emptyList(),
val bookmarks: List<UserBookmark> = emptyList()
)

View file

@ -219,8 +219,7 @@ override fun onSeekTo(pos: Long) {
)
playerNotificationService
.clientEventEmitter
?.onBookmarkDeleteRequested(
.requestAndroidAutoBookmarkDelete(
libraryItemId,
bookmarkTime
)

View file

@ -612,9 +612,17 @@ mediaSessionConnector.setMediaMetadataProvider { _ ->
currentPlaybackSession = playbackSession
// The "Capitoli" browser entry depends on the active playback session.
// Notify Android Auto so it reloads the application root.
// The Android Auto entries depend on the active playback session.
// Invalidate both the application root and the already-cached subtrees.
notifyChildrenChanged(AUTO_MEDIA_ROOT)
notifyChildrenChanged(ANDROID_AUTO_CHAPTERS_ROOT)
notifyChildrenChanged(ANDROID_AUTO_BOOKMARKS_ROOT)
playbackSession.libraryItemId?.let { libraryItemId ->
refreshAndroidAutoBookmarksFromServer(
libraryItemId
)
}
lastAndroidAutoChapterStartMs = null
@ -1159,6 +1167,105 @@ val actions =
* Returns the browsable Android Auto entry used to open the current
* audiobook's chapter list.
*/
private fun refreshAndroidAutoBookmarksFromServer(
libraryItemId: String
) {
apiHandler.getCurrentUser { user ->
val bookmarks =
user
?.bookmarks
?.filter {
it.libraryItemId == libraryItemId
}
?.map {
AndroidAutoBookmark(
title =
it.title
?.takeIf { title ->
title.isNotBlank()
}
?: "Segnalibro",
timeSeconds =
it.time
.toLong()
.coerceAtLeast(0L)
)
}
?: emptyList()
Handler(Looper.getMainLooper()).post {
if (
currentPlaybackSession?.libraryItemId ==
libraryItemId
) {
setAndroidAutoBookmarks(bookmarks)
Log.d(
tag,
"Android Auto native bookmarks loaded: " +
"libraryItemId=$libraryItemId, " +
"count=${bookmarks.size}"
)
}
}
}
}
fun requestAndroidAutoBookmarkDelete(
libraryItemId: String,
timeSeconds: Long
) {
val emitter = clientEventEmitter
if (emitter != null) {
emitter.onBookmarkDeleteRequested(
libraryItemId,
timeSeconds
)
return
}
Log.d(
tag,
"Android Auto native bookmark delete: " +
"libraryItemId=$libraryItemId, " +
"time=$timeSeconds"
)
apiHandler.deleteBookmark(
libraryItemId,
timeSeconds
) { success ->
Handler(Looper.getMainLooper()).post {
if (!success) {
Log.e(
tag,
"Android Auto native bookmark delete failed: " +
"libraryItemId=$libraryItemId, " +
"time=$timeSeconds"
)
return@post
}
androidAutoBookmarks =
androidAutoBookmarks.filterNot {
it.timeSeconds == timeSeconds
}
notifyChildrenChanged(AUTO_MEDIA_ROOT)
notifyChildrenChanged(
ANDROID_AUTO_BOOKMARKS_ROOT
)
Log.d(
tag,
"Android Auto native bookmark deleted: " +
"remaining=${androidAutoBookmarks.size}"
)
}
}
}
fun setAndroidAutoBookmarks(
bookmarks: List<AndroidAutoBookmark>
) {
@ -1747,10 +1854,56 @@ fun skipToNextChapter() {
"libraryItemId=$libraryItemId, time=$timeSeconds"
)
clientEventEmitter?.onBookmarkRequested(
val emitter = clientEventEmitter
if (emitter != null) {
emitter.onBookmarkRequested(
libraryItemId,
timeSeconds
)
return
}
val title = "Android Auto"
Log.d(
tag,
"Android Auto native bookmark create: " +
"libraryItemId=$libraryItemId, " +
"time=$timeSeconds"
)
apiHandler.createBookmark(
libraryItemId,
title,
timeSeconds
) { success ->
Handler(Looper.getMainLooper()).post {
if (!success) {
Log.e(
tag,
"Android Auto native bookmark create failed: " +
"libraryItemId=$libraryItemId, " +
"time=$timeSeconds"
)
return@post
}
setAndroidAutoBookmarks(
androidAutoBookmarks +
AndroidAutoBookmark(
title = title,
timeSeconds = timeSeconds
)
)
Log.d(
tag,
"Android Auto native bookmark created: " +
"count=${androidAutoBookmarks.size}"
)
}
}
}
fun setPlaybackSpeed(speed: Float) {
@ -2029,7 +2182,7 @@ lastAndroidAutoChapterStartMs = null
"time=$timeSeconds"
)
clientEventEmitter?.onBookmarkDeleteRequested(
requestAndroidAutoBookmarkDelete(
libraryItemId,
timeSeconds
)

View file

@ -106,6 +106,37 @@ class ApiHandler(var ctx:Context) {
}
}
private fun deleteRequest(
endpoint: String,
cb: (JSObject) -> Unit
) {
val requestUrl = "${DeviceManager.serverAddress}$endpoint"
try {
val request =
Request.Builder()
.delete()
.url(requestUrl)
.addHeader(
"Authorization",
"Bearer ${DeviceManager.token}"
)
.build()
makeRequest(request, null, cb)
} catch (error: Exception) {
Log.e(tag, "deleteRequest failed for $requestUrl", error)
val result = JSObject()
result.put(
"error",
"Request failed: ${error.message}"
)
cb(result)
}
}
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient
@ -135,8 +166,9 @@ class ApiHandler(var ctx:Context) {
return
}
val bodyString = it.body!!.string()
if (bodyString == "OK") {
val bodyString = it.body?.string().orEmpty()
if (bodyString.isBlank() || bodyString == "OK") {
cb(JSObject())
} else {
try {
@ -390,6 +422,38 @@ class ApiHandler(var ctx:Context) {
}
}
fun createBookmark(
libraryItemId: String,
title: String,
timeSeconds: Long,
cb: (Boolean) -> Unit
) {
val payload = JSObject()
payload.put("title", title)
payload.put("time", timeSeconds)
postRequest(
"/api/me/item/$libraryItemId/bookmark",
payload,
null
) { result ->
cb(!result.has("error"))
}
}
fun deleteBookmark(
libraryItemId: String,
timeSeconds: Long,
cb: (Boolean) -> Unit
) {
deleteRequest(
"/api/me/item/$libraryItemId/bookmark/$timeSeconds"
) { result ->
cb(!result.has("error"))
}
}
fun getCurrentUser(cb: (User?) -> Unit) {
getRequest("/api/me", null, null) {
if (it.has("error")) {

View file

@ -823,6 +823,14 @@ closeAndroidAutoDebug() {
this.showAndroidAutoDebug = false
},
async onBookmarkDeleteRequested(payload) {
console.log(
'[AA BOOKMARK DELETE REQUEST]',
JSON.stringify({
payload,
currentServerLibraryItemId: this.serverLibraryItemId
})
)
const libraryItemId =
payload?.libraryItemId || this.serverLibraryItemId
@ -842,6 +850,16 @@ closeAndroidAutoDebug() {
`/api/me/item/${libraryItemId}/bookmark/${time}`
)
console.log(
'[AA BOOKMARK DELETE API OK]',
JSON.stringify({
libraryItemId,
time,
bookmarksBefore:
this.$store.state.user?.user?.bookmarks || []
})
)
this.$store.commit(
'user/deleteBookmark',
{
@ -850,6 +868,16 @@ closeAndroidAutoDebug() {
}
)
console.log(
'[AA BOOKMARK DELETE STORE OK]',
JSON.stringify({
libraryItemId,
time,
bookmarksAfter:
this.$store.state.user?.user?.bookmarks || []
})
)
this.$toast.success('Bookmark removed')
} catch (error) {
this.$toast.error(
@ -904,6 +932,11 @@ closeAndroidAutoDebug() {
bookmark
)
this.$store.commit('user/updateBookmark', {
...bookmark,
libraryItemId
})
this.$toast.success('Bookmark added')
} catch (error) {
this.$toast.error(

View file

@ -146,7 +146,16 @@ export default {
}
this.$nativeHttp
.post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => {
.then((createdBookmark) => {
this.$store.commit(
'user/updateBookmark',
createdBookmark?.libraryItemId
? createdBookmark
: {
...bookmark,
libraryItemId: this.libraryItemId
}
)
this.$toast.success('Bookmark added')
})
.catch((error) => {

View file

@ -257,19 +257,33 @@ export const mutations = {
state.settings = settings
},
updateBookmark(state, bookmark) {
if (!state.user?.bookmarks) return
state.user.bookmarks = state.user.bookmarks.map((bm) => {
if (bm.libraryItemId === bookmark.libraryItemId && bm.time === bookmark.time) {
return bookmark
if (!state.user) return
const bookmarks = state.user.bookmarks || []
const bookmarkIndex = bookmarks.findIndex(
(bm) =>
bm.libraryItemId === bookmark.libraryItemId &&
Number(bm.time) === Number(bookmark.time)
)
if (bookmarkIndex === -1) {
state.user.bookmarks = [...bookmarks, bookmark]
return
}
return bm
})
state.user.bookmarks = bookmarks.map((bm, index) =>
index === bookmarkIndex ? bookmark : bm
)
},
deleteBookmark(state, { libraryItemId, time }) {
if (!state.user?.bookmarks) return
state.user.bookmarks = state.user.bookmarks.filter((bm) => {
if (bm.libraryItemId === libraryItemId && bm.time === time) return false
return true
})
state.user.bookmarks = state.user.bookmarks.filter(
(bm) =>
!(
bm.libraryItemId === libraryItemId &&
Number(bm.time) === Number(time)
)
)
}
}