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
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class User(
|
||||
val id:String,
|
||||
val username: String,
|
||||
val mediaProgress:List<MediaProgress>
|
||||
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> = emptyList(),
|
||||
val bookmarks: List<UserBookmark> = emptyList()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -219,8 +219,7 @@ override fun onSeekTo(pos: Long) {
|
|||
)
|
||||
|
||||
playerNotificationService
|
||||
.clientEventEmitter
|
||||
?.onBookmarkDeleteRequested(
|
||||
.requestAndroidAutoBookmarkDelete(
|
||||
libraryItemId,
|
||||
bookmarkTime
|
||||
)
|
||||
|
|
|
|||
|
|
@ -612,16 +612,24 @@ 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)
|
||||
|
||||
lastAndroidAutoChapterStartMs = null
|
||||
playbackSession.libraryItemId?.let { libraryItemId ->
|
||||
refreshAndroidAutoBookmarksFromServer(
|
||||
libraryItemId
|
||||
)
|
||||
}
|
||||
|
||||
if (isAndroidAutoPlayback()) {
|
||||
lastAndroidAutoChapterStartMs = null
|
||||
|
||||
if (isAndroidAutoPlayback()) {
|
||||
startAndroidAutoMediaSessionUpdates()
|
||||
updateAndroidAutoMediaSessionState()
|
||||
}
|
||||
}
|
||||
|
||||
DeviceManager.setLastPlaybackSession(
|
||||
playbackSession
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue