Add:Filter and sort for podcast episodes table, Update:Sync local media progress when media progress is updated on the server

This commit is contained in:
advplyr 2022-06-01 19:38:26 -05:00
parent c4aca22c28
commit 99bf960b8a
10 changed files with 283 additions and 10 deletions

View file

@ -42,4 +42,15 @@ data class LocalMediaProgress(
isFinished = playbackSession.progress >= 0.99
finishedAt = if (isFinished) lastUpdate else null
}
@JsonIgnore
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
isFinished = serverMediaProgress.isFinished
progress = serverMediaProgress.progress
currentTime = serverMediaProgress.currentTime
duration = serverMediaProgress.duration
lastUpdate = serverMediaProgress.lastUpdate
finishedAt = serverMediaProgress.finishedAt
startedAt = serverMediaProgress.startedAt
}
}

View file

@ -6,6 +6,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
@ -188,7 +189,7 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun removeLocalMediaProgress(call:PluginCall) {
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId)
call.resolve()
}
@ -204,6 +205,63 @@ class AbsDatabase : Plugin() {
}
}
// Updates received via web socket
// This function doesn't need to sync with the server also because this data is coming from the server
// If sending the localMediaProgressId then update existing media progress
// If sending localLibraryItemId then save new local media progress
@PluginMethod
fun syncServerMediaProgressWithLocalMediaProgress(call:PluginCall) {
val serverMediaProgress = call.getObject("mediaProgress").toString()
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
var localEpisodeId:String? = call.getString("localEpisodeId", "").toString()
if (localEpisodeId.isNullOrEmpty()) localEpisodeId = null
var localMediaProgressId = call.getString("localMediaProgressId") ?: ""
val mediaProgress = jacksonMapper.readValue<MediaProgress>(serverMediaProgress)
if (localMediaProgressId == "") {
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem != null) {
localMediaProgressId = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val localMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = mediaProgress.isFinished,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
serverAddress = localLibraryItem.serverAddress,
serverUserId = localLibraryItem.serverUserId,
libraryItemId = localLibraryItem.libraryItemId,
episodeId = mediaProgress.episodeId)
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress: Saving new local media progress $localMediaProgress")
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
} else {
Log.e(tag, "syncServerMediaProgressWithLocalMediaProgress: Local library item not found")
}
} else {
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress $localMediaProgressId")
val localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
call.resolve()
} else {
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
}
}
@PluginMethod
fun updateLocalMediaProgressFinished(call:PluginCall) {
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()

View file

@ -10,7 +10,7 @@
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
<div class="relative flex items-center px-3">
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
</div>
@ -30,7 +30,8 @@ export default {
items: {
type: Array,
default: () => []
}
},
selected: String // optional
},
data() {
return {}

View file

@ -1,10 +1,24 @@
<template>
<div class="w-full">
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodes.length }})</p>
<div class="flex items-center">
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodesFiltered.length }})</p>
<div class="flex-grow" />
<button class="outline:none mx-3 pt-0.5 relative" @click="showFilters">
<span class="material-icons text-xl text-gray-200">filter_alt</span>
<div v-show="filterKey !== 'all'" class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
</button>
<template v-for="episode in episodes">
<div class="flex items-center border border-white border-opacity-25 rounded px-2" @click="clickSort">
<p class="text-sm text-gray-200">{{ sortText }}</p>
<span class="material-icons ml-1 text-gray-200">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</div>
<template v-for="episode in episodesFiltered">
<tables-podcast-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :is-local="isLocal" :key="episode.id" />
</template>
<modals-dialog v-model="showFiltersModal" title="Episode Filter" :items="filterItems" :selected="filterKey" @action="setFilter" />
</div>
</template>
@ -24,9 +38,78 @@ export default {
isLocal: Boolean // If is local then episodes and libraryItemId are local, otherwise local is passed in localLibraryItemId and localEpisodes
},
data() {
return {}
return {
episodesCopy: [],
showFiltersModal: false,
sortKey: 'publishedAt',
sortDesc: false,
filterKey: 'all',
episodesFiltered: [],
episodeSortItems: [
{
text: 'Pub Date',
value: 'publishedAt'
},
{
text: 'Title',
value: 'title'
},
{
text: 'Season',
value: 'season'
},
{
text: 'Episode',
value: 'episode'
}
],
filterItems: [
{
text: 'Show All',
value: 'all'
},
{
text: 'Incomplete',
value: 'incomplete'
},
{
text: 'In Progress',
value: 'inProgress'
},
{
text: 'Complete',
value: 'complete'
}
]
}
},
watch: {
episodes: {
immediate: true,
handler() {
this.init()
}
},
filterKey: {
handler() {
this.setEpisodesFiltered()
}
},
sortKey: {
handler() {
this.setEpisodesFiltered()
}
}
},
computed: {
episodesSorted() {
return this.episodesFiltered.sort((a, b) => {
if (this.sortDesc) {
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
}
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
})
},
// Map of local episodes where server episode id is key
localEpisodeMap() {
var epmap = {}
@ -36,9 +119,60 @@ export default {
}
})
return epmap
},
sortText() {
if (!this.sortKey) return ''
var _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
if (!_sel) return ''
return _sel.text
}
},
methods: {
setEpisodesFiltered() {
this.episodesFiltered = this.episodesCopy
.filter((ep) => {
var mediaProgress = this.getEpisodeProgress(ep)
if (this.filterKey === 'incomplete') {
return !mediaProgress || !mediaProgress.isFinished
} else if (this.filterKey === 'complete') {
return mediaProgress && mediaProgress.isFinished
} else if (this.filterKey === 'inProgress') {
return mediaProgress && !mediaProgress.isFinished
} else if (this.filterKey === 'all') {
console.log('Filter key is all')
return true
}
return true
})
.sort((a, b) => {
if (this.sortDesc) {
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
}
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
})
},
setFilter(filter) {
this.filterKey = filter
console.log('Set filter', this.filterKey)
this.showFiltersModal = false
},
showFilters() {
this.showFiltersModal = true
},
clickSort() {
this.sortDesc = !this.sortDesc
},
getEpisodeProgress(episode) {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
},
init() {
this.episodesCopy = this.episodes.map((ep) => {
return { ...ep }
})
this.setEpisodesFiltered()
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -197,16 +197,66 @@ export default {
console.log('[default] syncLocalMediaProgress No local media progress to sync')
}
},
userUpdated(user) {
async userUpdated(user) {
if (this.user && this.user.id == user.id) {
this.$store.commit('user/setUser', user)
}
},
async userMediaProgressUpdated(prog) {
console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`)
// Update local media progress if exists
var localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](prog.libraryItemId, prog.episodeId)
var newLocalMediaProgress = null
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
// Server progress is more up-to-date
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''}`)
const payload = {
localMediaProgressId: localProg.id,
mediaProgress: prog
}
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
} else {
// Check if local library item exists
var localLibraryItem = await this.$db.getLocalLibraryItemByLLId(prog.libraryItemId)
if (localLibraryItem) {
if (prog.episodeId) {
// If episode check if local episode exists
var lliEpisodes = localLibraryItem.media.episodes || []
var localEpisode = lliEpisodes.find((ep) => ep.serverEpisodeId === prog.episodeId)
if (localEpisode) {
// Add new local media progress
const payload = {
localLibraryItemId: localLibraryItem.id,
localEpisodeId: localEpisode.id,
mediaProgress: prog
}
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
}
} else {
// Add new local media progress
const payload = {
localLibraryItemId: localLibraryItem.id,
mediaProgress: prog
}
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
}
} else {
console.log(`[default] userMediaProgressUpdate no local media progress or lli found for this server item ${prog.id}`)
}
}
if (newLocalMediaProgress && newLocalMediaProgress.id) {
console.log(`[default] local media progress updated for ${newLocalMediaProgress.id}`)
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
}
}
},
async mounted() {
this.$socket.on('connection-update', this.socketConnectionUpdate)
this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated)
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
if (this.$store.state.isFirstLoad) {
this.$store.commit('setIsFirstLoad', false)
@ -231,6 +281,7 @@ export default {
this.$socket.off('connection-update', this.socketConnectionUpdate)
this.$socket.off('initialized', this.socketInit)
this.$socket.off('user_updated', this.userUpdated)
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
}
}
</script>

View file

@ -82,7 +82,7 @@ export default {
console.log(libraryItemId)
if (libraryItemId.startsWith('local')) {
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
console.log('Got lli', libraryItem)
console.log('Got lli', libraryItemId)
} else if (store.state.user.serverConnectionConfig) {
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
console.error('Failed', error)
@ -283,7 +283,7 @@ export default {
if (this.isLocal) {
// TODO: If connected to server also sync with server
await this.$db.removeLocalMediaProgress(this.libraryItemId)
this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId)
this.$store.commit('globals/removeLocalMediaProgressForItem', this.libraryItemId)
} else {
var progressId = this.userItemProgress.id
await this.$axios

View file

@ -195,6 +195,10 @@ class AbsDatabaseWeb extends WebPlugin {
return null
}
async syncServerMediaProgressWithLocalMediaProgress(payload) {
return null
}
async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
return []
}

View file

@ -70,6 +70,10 @@ class DbService {
return AbsDatabase.syncLocalMediaProgressWithServer()
}
syncServerMediaProgressWithLocalMediaProgress(payload) {
return AbsDatabase.syncServerMediaProgressWithLocalMediaProgress(payload)
}
updateLocalTrackOrder(payload) {
return AbsDatabase.updateLocalTrackOrder(payload)
}

View file

@ -91,6 +91,7 @@ class ServerSocket extends EventEmitter {
console.log('[SOCKET] User Item Progress Updated', JSON.stringify(data))
var progress = data.data
this.$store.commit('user/updateUserMediaProgress', progress)
this.emit('user_media_progress_updated', progress)
}
}

View file

@ -36,6 +36,12 @@ export const getters = {
if (episodeId != null && lmp.localEpisodeId != episodeId) return false
return lmp.localLibraryItemId == localLibraryItemId
})
},
getLocalMediaProgressByServerItemId: (state) => (libraryItemId, episodeId = null) => {
return state.localMediaProgress.find(lmp => {
if (episodeId != null && lmp.episodeId != episodeId) return false
return lmp.libraryItemId == libraryItemId
})
}
}
@ -84,6 +90,9 @@ export const mutations = {
removeLocalMediaProgress(state, id) {
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id)
},
removeLocalMediaProgressForItem(state, llid) {
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.localLibraryItemId !== llid)
},
setLastSearch(state, val) {
state.lastSearch = val
}