mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-24 01:13:00 -04:00
Fix:Clean user data on server start removing invalid media progress items
This commit is contained in:
parent
9ee6eaade9
commit
ac30a971c5
@ -187,6 +187,11 @@ export default {
|
||||
this.$toast.error('Failed to start scan')
|
||||
})
|
||||
},
|
||||
userUpdated(user) {
|
||||
if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) {
|
||||
this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening)
|
||||
}
|
||||
},
|
||||
libraryItemAdded(libraryItem) {
|
||||
console.log('libraryItem added', libraryItem)
|
||||
// TODO: Check if libraryItem would be on this shelf
|
||||
@ -244,23 +249,16 @@ export default {
|
||||
this.libraryItemUpdated(li)
|
||||
})
|
||||
},
|
||||
seriesUpdated(series) {
|
||||
if (series.hideFromHome) {
|
||||
this.shelves.forEach((shelf) => {
|
||||
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
||||
// Filter out series books from continue series shelf
|
||||
shelf.entities = shelf.entities.filter((ent) => {
|
||||
if (ent.media.metadata.series && ent.media.metadata.series.id == series.id) return false
|
||||
return true
|
||||
})
|
||||
} else if (shelf.type == 'series') {
|
||||
// Filter out series from series shelf
|
||||
shelf.entities = shelf.entities.filter((ent) => {
|
||||
return ent.id != series.id
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
removeAllSeriesFromContinueSeries(seriesIds) {
|
||||
this.shelves.forEach((shelf) => {
|
||||
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
||||
// Filter out series books from continue series shelf
|
||||
shelf.entities = shelf.entities.filter((ent) => {
|
||||
if (ent.media.metadata.series && seriesIds.includes(ent.media.metadata.series.id)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
authorUpdated(author) {
|
||||
this.shelves.forEach((shelf) => {
|
||||
@ -288,7 +286,7 @@ export default {
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
if (this.$root.socket) {
|
||||
this.$root.socket.on('series_updated', this.seriesUpdated)
|
||||
this.$root.socket.on('user_updated', this.userUpdated)
|
||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||
@ -304,7 +302,7 @@ export default {
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||
|
||||
if (this.$root.socket) {
|
||||
this.$root.socket.off('series_updated', this.seriesUpdated)
|
||||
this.$root.socket.off('user_updated', this.userUpdated)
|
||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||
|
@ -28,7 +28,6 @@
|
||||
<span class="font-mono">{{ numShowing }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="seriesHideFromHome" :loading="processingSeriesHideFromHome" color="primary" small class="mr-2" @click="showSeriesOnHome">Show on Home</ui-btn>
|
||||
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center" @click="markSeriesFinished">
|
||||
<div class="h-5 w-5">
|
||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||
@ -95,8 +94,7 @@ export default {
|
||||
keywordTimeout: null,
|
||||
processingSeries: false,
|
||||
processingIssues: false,
|
||||
processingAuthors: false,
|
||||
processingSeriesHideFromHome: false
|
||||
processingAuthors: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -152,9 +150,6 @@ export default {
|
||||
seriesName() {
|
||||
return this.selectedSeries ? this.selectedSeries.name : null
|
||||
},
|
||||
seriesHideFromHome() {
|
||||
return this.selectedSeries ? this.selectedSeries.hideFromHome : null
|
||||
},
|
||||
seriesProgress() {
|
||||
return this.selectedSeries ? this.selectedSeries.progress : null
|
||||
},
|
||||
@ -176,23 +171,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async showSeriesOnHome() {
|
||||
this.processingSeriesHideFromHome = true
|
||||
const seriesId = this.selectedSeries.id
|
||||
this.$axios
|
||||
.$patch(`/api/series/${seriesId}`, { hideFromHome: false })
|
||||
.then((data) => {
|
||||
console.log('Updated series', data)
|
||||
this.$toast.success('Series updated successfully')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update series', error)
|
||||
this.$toast.error('Failed to update series')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingSeriesHideFromHome = false
|
||||
})
|
||||
},
|
||||
async matchAllAuthors() {
|
||||
this.processingAuthors = true
|
||||
|
||||
|
@ -411,10 +411,10 @@ export default {
|
||||
text: 'Re-Scan'
|
||||
})
|
||||
}
|
||||
if (this.userIsAdminOrUp && this.series && this.bookMount) {
|
||||
if (this.series && this.bookMount) {
|
||||
items.push({
|
||||
func: 'hideSeriesFromHome',
|
||||
text: 'Hide Series from Home'
|
||||
func: 'hideSeriesFromContinueListening',
|
||||
text: 'Hide Series from Continue Series'
|
||||
})
|
||||
}
|
||||
return items
|
||||
@ -595,17 +595,17 @@ export default {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
|
||||
},
|
||||
hideSeriesFromHome() {
|
||||
hideSeriesFromContinueListening() {
|
||||
const axios = this.$axios || this.$nuxt.$axios
|
||||
this.processing = true
|
||||
axios
|
||||
.$patch(`/api/series/${this.series.id}`, { hideFromHome: true })
|
||||
.$post(`/api/me/series/${this.series.id}/hide`)
|
||||
.then((data) => {
|
||||
console.log('Series updated', data)
|
||||
console.log('User updated', data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to hide series from home', error)
|
||||
this.$toast.error('Failed to update series')
|
||||
this.$toast.error('Failed to update user')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
@ -145,7 +145,7 @@ class Server {
|
||||
await this.auth.initTokenSecret()
|
||||
}
|
||||
|
||||
await this.checkUserMediaProgress() // Remove invalid user item progress
|
||||
await this.cleanUserData() // Remove invalid user item progress
|
||||
await this.purgeMetadata() // Remove metadata folders without library item
|
||||
await this.playbackSessionManager.removeInvalidSessions()
|
||||
await this.cacheManager.ensureCachePaths()
|
||||
@ -368,21 +368,37 @@ class Server {
|
||||
return purged
|
||||
}
|
||||
|
||||
// Remove user media progress entries that dont have a library item
|
||||
// TODO: Check podcast episode exists still
|
||||
async checkUserMediaProgress() {
|
||||
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
||||
async cleanUserData() {
|
||||
for (let i = 0; i < this.db.users.length; i++) {
|
||||
var _user = this.db.users[i]
|
||||
if (_user.mediaProgress) {
|
||||
var itemProgressIdsToRemove = _user.mediaProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
|
||||
if (itemProgressIdsToRemove.length) {
|
||||
Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} media progress data to remove from user ${_user.username}`)
|
||||
for (const lipId of itemProgressIdsToRemove) {
|
||||
_user.removeMediaProgress(lipId)
|
||||
}
|
||||
await this.db.updateEntity('user', _user)
|
||||
var hasUpdated = false
|
||||
if (_user.mediaProgress.length) {
|
||||
const lengthBefore = _user.mediaProgress.length
|
||||
_user.mediaProgress = _user.mediaProgress.filter(mp => {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||
if (!libraryItem) return false
|
||||
if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
|
||||
return true
|
||||
})
|
||||
|
||||
if (lengthBefore > _user.mediaProgress.length) {
|
||||
Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
if (_user.seriesHideFromContinueListening.length) {
|
||||
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
||||
if (!this.db.series.some(se => se.id === seriesId)) { // Series removed
|
||||
hasUpdated = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', _user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,5 +276,21 @@ class MeController {
|
||||
libraryItems: itemsInProgress
|
||||
})
|
||||
}
|
||||
|
||||
// GET: api/me/series/:id/hide
|
||||
async hideSeriesFromContinueListening(req, res) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] hideSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
}
|
||||
}
|
||||
module.exports = new MeController()
|
@ -5,7 +5,6 @@ class Series {
|
||||
this.id = null
|
||||
this.name = null
|
||||
this.description = null
|
||||
this.hideFromHome = false
|
||||
this.addedAt = null
|
||||
this.updatedAt = null
|
||||
|
||||
@ -18,7 +17,6 @@ class Series {
|
||||
this.id = series.id
|
||||
this.name = series.name
|
||||
this.description = series.description || null
|
||||
this.hideFromHome = !!series.hideFromHome
|
||||
this.addedAt = series.addedAt
|
||||
this.updatedAt = series.updatedAt
|
||||
}
|
||||
@ -28,7 +26,6 @@ class Series {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
hideFromHome: this.hideFromHome,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt
|
||||
}
|
||||
@ -46,14 +43,13 @@ class Series {
|
||||
this.id = getId('ser')
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
this.hideFromHome = !!data.hideFromHome
|
||||
this.addedAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
}
|
||||
|
||||
update(series) {
|
||||
if (!series) return false
|
||||
const keysToUpdate = ['name', 'description', 'hideFromHome']
|
||||
const keysToUpdate = ['name', 'description']
|
||||
var hasUpdated = false
|
||||
for (const key of keysToUpdate) {
|
||||
if (series[key] !== undefined && series[key] !== this[key]) {
|
||||
|
@ -15,6 +15,7 @@ class User {
|
||||
this.createdAt = null
|
||||
|
||||
this.mediaProgress = []
|
||||
this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening
|
||||
this.bookmarks = []
|
||||
|
||||
this.settings = {}
|
||||
@ -92,6 +93,7 @@ class User {
|
||||
type: this.type,
|
||||
token: this.token,
|
||||
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
|
||||
seriesHideFromContinueListening: [...this.seriesHideFromContinueListening],
|
||||
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
||||
isActive: this.isActive,
|
||||
isLocked: this.isLocked,
|
||||
@ -111,6 +113,7 @@ class User {
|
||||
type: this.type,
|
||||
token: this.token,
|
||||
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
|
||||
seriesHideFromContinueListening: [...this.seriesHideFromContinueListening],
|
||||
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
||||
isActive: this.isActive,
|
||||
isLocked: this.isLocked,
|
||||
@ -161,6 +164,9 @@ class User {
|
||||
this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm))
|
||||
}
|
||||
|
||||
this.seriesHideFromContinueListening = []
|
||||
if (user.seriesHideFromContinueListening) this.seriesHideFromContinueListening = [...user.seriesHideFromContinueListening]
|
||||
|
||||
this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive
|
||||
this.isLocked = user.type === 'root' ? false : !!user.isLocked
|
||||
this.lastSeen = user.lastSeen || null
|
||||
@ -196,6 +202,13 @@ class User {
|
||||
}
|
||||
})
|
||||
|
||||
if (payload.seriesHideFromContinueListening && Array.isArray(payload.seriesHideFromContinueListening)) {
|
||||
if (this.seriesHideFromContinueListening.join(',') !== payload.seriesHideFromContinueListening.join(',')) {
|
||||
hasUpdates = true
|
||||
this.seriesHideFromContinueListening = [...payload.seriesHideFromContinueListening]
|
||||
}
|
||||
}
|
||||
|
||||
// And update permissions
|
||||
if (payload.permissions) {
|
||||
for (const key in payload.permissions) {
|
||||
@ -297,7 +310,13 @@ class User {
|
||||
return wasUpdated
|
||||
}
|
||||
|
||||
removeMediaProgress(libraryItemId) {
|
||||
removeMediaProgress(id) {
|
||||
if (!this.mediaProgress.some(mp => mp.id === id)) return false
|
||||
this.mediaProgress = this.mediaProgress.filter(mp => mp.id !== id)
|
||||
return true
|
||||
}
|
||||
|
||||
removeMediaProgressForLibraryItem(libraryItemId) {
|
||||
if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false
|
||||
this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId)
|
||||
return true
|
||||
@ -378,5 +397,15 @@ class User {
|
||||
removeBookmark(libraryItemId, time) {
|
||||
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
|
||||
}
|
||||
|
||||
checkShouldHideSeriesFromContinueListening(seriesId) {
|
||||
return this.seriesHideFromContinueListening.includes(seriesId)
|
||||
}
|
||||
|
||||
addSeriesToHideFromContinueListening(seriesId) {
|
||||
if (this.seriesHideFromContinueListening.includes(seriesId)) return false
|
||||
this.seriesHideFromContinueListening.push(seriesId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
module.exports = User
|
@ -150,6 +150,7 @@ class ApiRouter {
|
||||
this.router.patch('/me/settings', MeController.updateSettings.bind(this))
|
||||
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
||||
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
|
||||
this.router.post('/me/series/:id/hide', MeController.hideSeriesFromContinueListening.bind(this))
|
||||
|
||||
//
|
||||
// Backup Routes
|
||||
@ -304,7 +305,7 @@ class ApiRouter {
|
||||
// Remove libraryItem from users
|
||||
for (let i = 0; i < this.db.users.length; i++) {
|
||||
var user = this.db.users[i]
|
||||
var madeUpdates = user.removeMediaProgress(libraryItem.id)
|
||||
var madeUpdates = user.removeMediaProgressForLibraryItem(libraryItem.id)
|
||||
if (madeUpdates) {
|
||||
await this.db.updateEntity('user', user)
|
||||
}
|
||||
|
@ -415,13 +415,16 @@ module.exports = {
|
||||
const libraryItemJson = libraryItem.toJSONMinified()
|
||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||
|
||||
const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id)
|
||||
|
||||
if (!seriesMap[librarySeries.id]) {
|
||||
const seriesObj = allSeries.find(se => se.id === librarySeries.id)
|
||||
if (seriesObj && !seriesObj.hideFromHome) {
|
||||
if (seriesObj) {
|
||||
var series = {
|
||||
...seriesObj.toJSON(),
|
||||
books: [libraryItemJson],
|
||||
inProgress: bookInProgress,
|
||||
hideFromContinueListening,
|
||||
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||
firstBookUnread: bookInProgress ? null : libraryItemJson
|
||||
}
|
||||
@ -555,7 +558,7 @@ module.exports = {
|
||||
|
||||
// For Continue Series - Find next book in series for series that are in progress
|
||||
for (const seriesId in seriesMap) {
|
||||
if (seriesMap[seriesId].inProgress) {
|
||||
if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) {
|
||||
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
||||
|
||||
// NEW implementation takes the first book unread with the smallest series sequence
|
||||
|
Loading…
x
Reference in New Issue
Block a user