Fix Android Auto chapter and bookmark synchronization
This commit is contained in:
parent
4cb8cf9578
commit
867b4467bc
7 changed files with 311 additions and 31 deletions
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -219,8 +219,7 @@ override fun onSeekTo(pos: Long) {
|
||||||
)
|
)
|
||||||
|
|
||||||
playerNotificationService
|
playerNotificationService
|
||||||
.clientEventEmitter
|
.requestAndroidAutoBookmarkDelete(
|
||||||
?.onBookmarkDeleteRequested(
|
|
||||||
libraryItemId,
|
libraryItemId,
|
||||||
bookmarkTime
|
bookmarkTime
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue