mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Add:Purge media progress button & api endpoint for items that no longer exist #921
This commit is contained in:
parent
97da73baf3
commit
162a1b7971
@ -46,7 +46,14 @@
|
|||||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Saved Media Progress</h1>
|
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Saved Media Progress</h1>
|
||||||
<table v-if="mediaProgress.length" class="userAudiobooksTable">
|
|
||||||
|
<div v-if="mediaProgressWithoutMedia.length" class="flex items-center py-2 mb-2">
|
||||||
|
<p class="text-error">User has media progress for {{ mediaProgressWithoutMedia.length }} items that no longer exist.</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn small :loading="purgingMediaProgress" @click.stop="purgeMediaProgress">Purge Media Progress</ui-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable">
|
||||||
<tr class="bg-primary bg-opacity-40">
|
<tr class="bg-primary bg-opacity-40">
|
||||||
<th class="w-16 text-left">Item</th>
|
<th class="w-16 text-left">Item</th>
|
||||||
<th class="text-left"></th>
|
<th class="text-left"></th>
|
||||||
@ -54,13 +61,19 @@
|
|||||||
<th class="w-40 hidden sm:table-cell">Started At</th>
|
<th class="w-40 hidden sm:table-cell">Started At</th>
|
||||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||||
<td>
|
<td>
|
||||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td class="font-book">
|
||||||
<p>{{ item.media && item.media.metadata ? item.media.metadata.title : 'Unknown' }}</p>
|
<template v-if="item.media && item.media.metadata && item.episode">
|
||||||
<p v-if="item.media && item.media.metadata && item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
|
<p>{{ item.episode.title || 'Unknown' }}</p>
|
||||||
|
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.media && item.media.metadata">
|
||||||
|
<p>{{ item.media.metadata.title || 'Unknown' }}</p>
|
||||||
|
<p v-if="item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
||||||
@ -98,7 +111,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listeningSessions: [],
|
listeningSessions: [],
|
||||||
listeningStats: {}
|
listeningStats: {},
|
||||||
|
purgingMediaProgress: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -117,6 +131,12 @@ export default {
|
|||||||
mediaProgress() {
|
mediaProgress() {
|
||||||
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
},
|
},
|
||||||
|
mediaProgressWithMedia() {
|
||||||
|
return this.mediaProgress.filter((mp) => mp.media)
|
||||||
|
},
|
||||||
|
mediaProgressWithoutMedia() {
|
||||||
|
return this.mediaProgress.filter((mp) => !mp.media)
|
||||||
|
},
|
||||||
totalListeningTime() {
|
totalListeningTime() {
|
||||||
return this.listeningStats.totalTime || 0
|
return this.listeningStats.totalTime || 0
|
||||||
},
|
},
|
||||||
@ -150,6 +170,24 @@ export default {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Loaded user listening data', this.listeningSessions, this.listeningStats)
|
console.log('Loaded user listening data', this.listeningSessions, this.listeningStats)
|
||||||
|
},
|
||||||
|
purgeMediaProgress() {
|
||||||
|
this.purgingMediaProgress = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/users/${this.user.id}/purge-media-progress`)
|
||||||
|
.then((updatedUser) => {
|
||||||
|
console.log('Updated user', updatedUser)
|
||||||
|
this.$toast.success('Media progress purged')
|
||||||
|
this.user = updatedUser
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to purge media progress', error)
|
||||||
|
this.$toast.error('Failed to purge media progress')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.purgingMediaProgress = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -40,13 +40,15 @@ export const getters = {
|
|||||||
// Absolute URL covers (should no longer be used)
|
// Absolute URL covers (should no longer be used)
|
||||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||||
|
|
||||||
var userToken = rootGetters['user/getToken']
|
const userToken = rootGetters['user/getToken']
|
||||||
var lastUpdate = libraryItem.updatedAt || Date.now()
|
const lastUpdate = libraryItem.updatedAt || Date.now()
|
||||||
|
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `http://localhost:3333/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
}
|
}
|
||||||
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
|
||||||
|
return `/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
},
|
},
|
||||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = '/book_placeholder.jpg') => {
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = '/book_placeholder.jpg') => {
|
||||||
if (!libraryItemId) return placeholder
|
if (!libraryItemId) return placeholder
|
||||||
|
@ -28,10 +28,6 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.warn('Non-admin user attempted to create user', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var account = req.body
|
var account = req.body
|
||||||
|
|
||||||
var username = account.username
|
var username = account.username
|
||||||
@ -58,15 +54,7 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
var user = req.reqUser
|
||||||
Logger.error('[UserController] User other than admin attempting to update user', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = this.db.users.find(u => u.id === req.params.id)
|
|
||||||
if (!user) {
|
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.type === 'root' && !req.user.isRoot) {
|
if (user.type === 'root' && !req.user.isRoot) {
|
||||||
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
||||||
@ -97,9 +85,9 @@ class UserController {
|
|||||||
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
|
this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
user: user.toJSONForBrowser()
|
user: user.toJSONForBrowser()
|
||||||
@ -107,24 +95,15 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.error('User other than admin attempting to delete user', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
if (req.params.id === 'root') {
|
if (req.params.id === 'root') {
|
||||||
|
Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted')
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
if (req.user.id === req.params.id) {
|
if (req.user.id === req.params.id) {
|
||||||
Logger.error('Attempting to delete themselves...')
|
Logger.error(`[UserController] ${req.user.username} is attempting to delete themselves... why? WHY?`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
var user = this.db.users.find(u => u.id === req.params.id)
|
var user = req.reqUser
|
||||||
if (!user) {
|
|
||||||
Logger.error('User not found')
|
|
||||||
return res.json({
|
|
||||||
error: 'User not found'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete user collections
|
// delete user collections
|
||||||
var userCollections = this.db.collections.filter(c => c.userId === user.id)
|
var userCollections = this.db.collections.filter(c => c.userId === user.id)
|
||||||
@ -145,10 +124,6 @@ class UserController {
|
|||||||
|
|
||||||
// GET: api/users/:id/listening-sessions
|
// GET: api/users/:id/listening-sessions
|
||||||
async getListeningSessions(req, res) {
|
async getListeningSessions(req, res) {
|
||||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||||
|
|
||||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||||
@ -170,11 +145,59 @@ class UserController {
|
|||||||
|
|
||||||
// GET: api/users/:id/listening-stats
|
// GET: api/users/:id/listening-stats
|
||||||
async getListeningStats(req, res) {
|
async getListeningStats(req, res) {
|
||||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
|
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
|
||||||
res.json(listeningStats)
|
res.json(listeningStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/users/:id/purge-media-progress
|
||||||
|
async purgeMediaProgress(req, res) {
|
||||||
|
const user = req.reqUser
|
||||||
|
|
||||||
|
if (user.type === 'root' && !req.user.isRoot) {
|
||||||
|
Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressPurged = 0
|
||||||
|
user.mediaProgress = user.mediaProgress.filter(mp => {
|
||||||
|
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
progressPurged++
|
||||||
|
return false
|
||||||
|
} else if (mp.episodeId) {
|
||||||
|
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
||||||
|
if (!episode) { // Episode not found
|
||||||
|
progressPurged++
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (progressPurged) {
|
||||||
|
Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
||||||
|
await this.db.updateEntity('user', user)
|
||||||
|
this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.params.id) {
|
||||||
|
req.reqUser = this.db.users.find(u => u.id === req.params.id)
|
||||||
|
if (!req.reqUser) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new UserController()
|
module.exports = new UserController()
|
@ -109,14 +109,15 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
//
|
//
|
||||||
this.router.post('/users', UserController.create.bind(this))
|
this.router.post('/users', UserController.middleware.bind(this), UserController.create.bind(this))
|
||||||
this.router.get('/users', UserController.findAll.bind(this))
|
this.router.get('/users', UserController.middleware.bind(this), UserController.findAll.bind(this))
|
||||||
this.router.get('/users/:id', UserController.findOne.bind(this))
|
this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this))
|
||||||
this.router.patch('/users/:id', UserController.update.bind(this))
|
this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this))
|
||||||
this.router.delete('/users/:id', UserController.delete.bind(this))
|
this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this))
|
||||||
|
|
||||||
this.router.get('/users/:id/listening-sessions', UserController.getListeningSessions.bind(this))
|
this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this))
|
||||||
this.router.get('/users/:id/listening-stats', UserController.getListeningStats.bind(this))
|
this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this))
|
||||||
|
this.router.post('/users/:id/purge-media-progress', UserController.middleware.bind(this), UserController.purgeMediaProgress.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Collection Routes
|
// Collection Routes
|
||||||
@ -274,12 +275,24 @@ class ApiRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
json.mediaProgress = json.mediaProgress.map(lip => {
|
json.mediaProgress = json.mediaProgress.map(lip => {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
var libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id)
|
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
|
||||||
return null
|
lip.media = null
|
||||||
|
} else {
|
||||||
|
if (lip.episodeId) {
|
||||||
|
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(lip.episodeId) : null
|
||||||
|
if (!episode) {
|
||||||
|
Logger.warn(`[ApiRouter] Episode ${lip.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`)
|
||||||
|
lip.media = null
|
||||||
|
} else {
|
||||||
|
lip.media = libraryItem.media.toJSONExpanded()
|
||||||
|
lip.episode = episode.toJSON()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lip.media = libraryItem.media.toJSONExpanded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lip.media = libraryItem.media.toJSONExpanded()
|
|
||||||
return lip
|
return lip
|
||||||
}).filter(lip => !!lip)
|
}).filter(lip => !!lip)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user