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

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

View file

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

View file

@ -612,16 +612,24 @@ mediaSessionConnector.setMediaMetadataProvider { _ ->
currentPlaybackSession = playbackSession currentPlaybackSession = playbackSession
// The "Capitoli" browser entry depends on the active playback session. // The Android Auto entries depend on the active playback session.
// Notify Android Auto so it reloads the application root. // Invalidate both the application root and the already-cached subtrees.
notifyChildrenChanged(AUTO_MEDIA_ROOT) notifyChildrenChanged(AUTO_MEDIA_ROOT)
notifyChildrenChanged(ANDROID_AUTO_CHAPTERS_ROOT)
notifyChildrenChanged(ANDROID_AUTO_BOOKMARKS_ROOT)
lastAndroidAutoChapterStartMs = null playbackSession.libraryItemId?.let { libraryItemId ->
refreshAndroidAutoBookmarksFromServer(
libraryItemId
)
}
if (isAndroidAutoPlayback()) { lastAndroidAutoChapterStartMs = null
startAndroidAutoMediaSessionUpdates()
updateAndroidAutoMediaSessionState() if (isAndroidAutoPlayback()) {
} startAndroidAutoMediaSessionUpdates()
updateAndroidAutoMediaSessionState()
}
DeviceManager.setLastPlaybackSession( DeviceManager.setLastPlaybackSession(
playbackSession playbackSession
@ -1159,6 +1167,105 @@ val actions =
* Returns the browsable Android Auto entry used to open the current * Returns the browsable Android Auto entry used to open the current
* audiobook's chapter list. * 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( fun setAndroidAutoBookmarks(
bookmarks: List<AndroidAutoBookmark> bookmarks: List<AndroidAutoBookmark>
) { ) {
@ -1747,10 +1854,56 @@ fun skipToNextChapter() {
"libraryItemId=$libraryItemId, time=$timeSeconds" "libraryItemId=$libraryItemId, time=$timeSeconds"
) )
clientEventEmitter?.onBookmarkRequested( val emitter = clientEventEmitter
libraryItemId,
timeSeconds 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) { fun setPlaybackSpeed(speed: Float) {
@ -2029,7 +2182,7 @@ lastAndroidAutoChapterStartMs = null
"time=$timeSeconds" "time=$timeSeconds"
) )
clientEventEmitter?.onBookmarkDeleteRequested( requestAndroidAutoBookmarkDelete(
libraryItemId, libraryItemId,
timeSeconds 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) { private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient val client = httpClient ?: defaultClient
@ -135,8 +166,9 @@ class ApiHandler(var ctx:Context) {
return return
} }
val bodyString = it.body!!.string() val bodyString = it.body?.string().orEmpty()
if (bodyString == "OK") {
if (bodyString.isBlank() || bodyString == "OK") {
cb(JSObject()) cb(JSObject())
} else { } else {
try { 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) { fun getCurrentUser(cb: (User?) -> Unit) {
getRequest("/api/me", null, null) { getRequest("/api/me", null, null) {
if (it.has("error")) { if (it.has("error")) {

View file

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

View file

@ -146,7 +146,16 @@ export default {
} }
this.$nativeHttp this.$nativeHttp
.post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark) .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') this.$toast.success('Bookmark added')
}) })
.catch((error) => { .catch((error) => {

View file

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