Compare commits

...

11 Commits

31 changed files with 271 additions and 134 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian
pkg -t node12-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb --build dist/debian
+3 -3
View File
@@ -30,7 +30,7 @@
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
</nuxt-link>
<nuxt-link v-if="isRootUser" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
</nuxt-link>
@@ -100,8 +100,8 @@ export default {
user() {
return this.$store.state.user.user
},
isRootUser() {
return this.$store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
username() {
return this.user ? this.user.username : 'err'
@@ -7,9 +7,9 @@
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
</div>
<div v-if="loaded && !shelves.length && isRootUser && !search" class="w-full flex flex-col items-center justify-center py-12">
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
<div class="flex">
<div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
</div>
@@ -44,8 +44,8 @@ export default {
}
},
computed: {
isRootUser() {
return this.$store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
+6 -6
View File
@@ -4,12 +4,12 @@
<div class="w-full h-full pt-6">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
</template>
</div>
<div v-if="shelf.type === 'episode'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editEpisode" />
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @editPodcast="editItem" @edit="editEpisode" />
</template>
</div>
<div v-if="shelf.type === 'series'" class="flex items-center">
@@ -101,10 +101,10 @@ export default {
this.selectedAuthor = author
this.showAuthorModal = true
},
editBook(audiobook) {
var bookIds = this.shelf.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', audiobook)
editItem(libraryItem) {
var itemIds = this.shelf.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setSelectedLibraryItem', libraryItem)
+3 -3
View File
@@ -25,11 +25,11 @@ export default {
return {}
},
computed: {
userIsRoot() {
return this.$store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
configRoutes() {
if (!this.userIsRoot) {
if (!this.userIsAdminOrUp) {
return [
{
id: 'config-stats',
+4 -4
View File
@@ -6,9 +6,9 @@
</div>
</template>
<div v-if="initialized && !totalShelves && !hasFilter && isRootUser && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
<div class="flex">
<div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
</div>
@@ -79,8 +79,8 @@ export default {
}
},
computed: {
isRootUser() {
return this.$store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
+28 -6
View File
@@ -60,7 +60,8 @@
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<div ref="moreIcon" v-show="!isSelectionMode && !recentEpisode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<!-- More Menu Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-100" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
@@ -255,8 +256,9 @@ export default {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
return null
},
episodeProgress() {
@@ -341,10 +343,23 @@ export default {
userCanDownload() {
return this.store.getters['user/getUserCanDownload']
},
userIsRoot() {
return this.store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
moreMenuItems() {
if (this.recentEpisode) {
return [
{
func: 'editPodcast',
text: 'Edit Podcast'
},
{
func: 'toggleFinished',
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
}
]
}
var items = []
if (!this.isPodcast) {
items = [
@@ -368,7 +383,7 @@ export default {
text: 'Match'
})
}
if (this.userIsRoot && !this.isFile) {
if (this.userIsAdminOrUp && !this.isFile) {
items.push({
func: 'rescan',
text: 'Re-Scan'
@@ -447,10 +462,14 @@ export default {
isFinished: !this.itemIsFinished
}
this.isProcessingReadUpdate = true
var apiEndpoint = `/api/me/progress/${this.libraryItemId}`
if (this.recentEpisode) apiEndpoint += `/${this.recentEpisode.id}`
var toast = this.$toast || this.$nuxt.$toast
var axios = this.$axios || this.$nuxt.$axios
axios
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
.$patch(apiEndpoint, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
@@ -461,6 +480,9 @@ export default {
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
},
editPodcast() {
this.$emit('editPodcast', this.libraryItem)
},
rescan() {
this.rescanning = true
this.$axios
@@ -52,6 +52,10 @@ export default {
text: 'Size',
value: 'size'
},
{
text: 'Duration',
value: 'media.duration'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
@@ -78,6 +82,10 @@ export default {
text: 'Size',
value: 'size'
},
{
text: '# of Episodes',
value: 'media.numTracks'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
@@ -112,8 +112,10 @@ export default {
return null
})
if (result) {
if (result.updated) this.$toast.success('Author updated')
else this.$toast.info('No updates were needed')
if (result.updated) {
this.$toast.success('Author updated')
this.show = false
} else this.$toast.info('No updates were needed')
}
this.processing = false
},
@@ -10,11 +10,11 @@
<div class="flex-grow" />
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
<ui-btn v-if="isRootUser" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
</ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
<ui-btn v-if="isRootUser && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
</ui-tooltip>
<ui-btn @click="submitForm">Submit</ui-btn>
@@ -52,8 +52,8 @@ export default {
isFile() {
return !!this.libraryItem && this.libraryItem.isFile
},
isRootUser() {
return this.$store.getters['user/getIsRoot']
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
isMissing() {
return !!this.libraryItem && !!this.libraryItem.isMissing
+63 -6
View File
@@ -6,18 +6,27 @@
</div>
</template>
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div class="w-full">
<div v-if="currentFeedUrl" class="w-full">
<p class="text-lg font-semibold mb-4">Podcast RSS Feed is Open</p>
<div class="w-full relative">
<ui-text-input v-model="feedUrl" readonly />
<ui-text-input v-model="currentFeedUrl" readonly />
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeedUrl)">content_copy</span>
</div>
</div>
<div v-else class="w-full">
<p class="text-lg font-semibold mb-4">Open RSS Feed</p>
<div class="w-full relative">
<ui-text-input-with-label v-model="newFeedSlug" label="RSS Feed Slug" />
<p class="text-xs text-gray-400 py-0.5 px-1">Feed will be {{ demoFeedUrl }}</p>
</div>
</div>
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
<div class="flex-grow" />
<ui-btn color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
</div>
</div>
</modals-modal>
@@ -35,7 +44,9 @@ export default {
},
data() {
return {
processing: false
processing: false,
newFeedSlug: null,
currentFeedUrl: null
}
},
watch: {
@@ -57,6 +68,9 @@ export default {
this.$emit('input', val)
}
},
libraryItemId() {
return this.libraryItem.id
},
media() {
return this.libraryItem.media || {}
},
@@ -68,9 +82,48 @@ export default {
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
demoFeedUrl() {
return `${window.origin}/feed/${this.newFeedSlug}`
}
},
methods: {
openFeed() {
if (!this.newFeedSlug) {
this.$toast.error('Must set a feed slug')
return
}
var sanitized = this.$sanitizeSlug(this.newFeedSlug)
if (this.newFeedSlug !== sanitized) {
this.newFeedSlug = sanitized
this.$toast.warning('Slug had to be modified - Run again')
return
}
const payload = {
serverAddress: window.origin,
slug: this.newFeedSlug
}
if (this.$isDev) payload.serverAddress = 'http://localhost:3333'
console.log('Payload', payload)
this.$axios
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
.then((data) => {
if (data.success) {
console.log('Opened RSS Feed', data)
this.currentFeedUrl = data.feedUrl
} else {
const errorMsg = data.error || 'Unknown error'
this.$toast.error(errorMsg)
}
})
.catch((error) => {
console.error('Failed to open RSS Feed', error)
this.$toast.error()
})
},
copyToClipboard(str) {
this.$copyToClipboard(str, this)
},
@@ -89,7 +142,11 @@ export default {
this.$toast.error()
})
},
init() {}
init() {
if (!this.libraryItem) return
this.newFeedSlug = this.libraryItem.id
this.currentFeedUrl = this.feedUrl
}
},
mounted() {}
}
+5 -2
View File
@@ -45,8 +45,8 @@
</td>
<td class="py-0">
<div class="w-full flex justify-center">
<!-- <span class="material-icons hover:text-gray-400 cursor-pointer text-base pr-2" @click.stop="editUser(user)">edit</span> -->
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
<!-- Dont show edit for non-root users -->
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
<span class="material-icons text-base">edit</span>
</div>
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
@@ -76,6 +76,9 @@ export default {
currentUserId() {
return this.$store.state.user.user.id
},
userIsRoot() {
return this.$store.getters['user/getIsRoot']
},
usersOnline() {
var usermap = {}
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
+16 -5
View File
@@ -112,11 +112,22 @@ export default {
items: []
})
var newtreemap = currtreemap.items[currtreemap.items.length - 1]
dirReader.readEntries((entries) => {
let entriesPromises = []
for (let entr of entries) entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
resolve(Promise.all(entriesPromises))
})
let entriesPromises = []
// readEntries returns 100 items max, continue calling readEntries until empty
function readEntries() {
dirReader.readEntries((entries) => {
if (entries.length > 0) {
for (let entr of entries) {
entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
}
readEntries()
} else {
resolve(Promise.all(entriesPromises))
}
})
}
readEntries()
}
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.0.9",
"version": "2.0.10",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
-3
View File
@@ -134,9 +134,6 @@ export default {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
isRootUser() {
return this.$store.getters['user/getIsRoot']
},
media() {
return this.libraryItem.media || {}
},
+1 -1
View File
@@ -15,7 +15,7 @@
<script>
export default {
asyncData({ store, redirect, route }) {
if (!store.getters['user/getIsRoot']) {
if (!store.getters['user/getIsAdminOrUp']) {
// Non-Root user only has access to the listening stats page
if (route.name !== 'config-stats') {
redirect('/config/stats')
-5
View File
@@ -34,11 +34,6 @@
<script>
export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsRoot']) {
redirect('/?error=unauthorized')
}
},
data() {
return {
search: null,
+1 -1
View File
@@ -14,7 +14,7 @@
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
<p class="py-2 text-xs">
<p v-if="userToken" class="py-2 text-xs">
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
><span class="material-icons pl-2 text-base">content_copy</span>
</p>
+1 -27
View File
@@ -492,33 +492,7 @@ export default {
this.$store.commit('globals/setShowUserCollectionsModal', true)
},
clickRSSFeed() {
if (!this.rssFeedUrl) {
if (confirm(`Are you sure you want to open an RSS Feed for this podcast?`)) {
this.openRSSFeed()
}
} else {
this.showRssFeedModal = true
}
},
openRSSFeed() {
const payload = {
serverAddress: window.origin
}
if (this.$isDev) payload.serverAddress = 'http://localhost:3333'
console.log('Payload', payload)
this.$axios
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
.then((data) => {
if (data.success) {
console.log('Opened RSS Feed', data)
this.rssFeedUrl = data.feedUrl
this.showRssFeedModal = true
}
})
.catch((error) => {
console.error('Failed to open RSS Feed', error)
})
this.showRssFeedModal = true
},
episodeDownloadQueued(episodeDownload) {
if (episodeDownload.libraryItemId === this.libraryItemId) {
+25
View File
@@ -125,6 +125,31 @@ Vue.prototype.$sanitizeFilename = (input, replacement = '') => {
return sanitized
}
// SOURCE: https://gist.github.com/spyesx/561b1d65d4afb595f295
// modified: allowed underscores
Vue.prototype.$sanitizeSlug = (str) => {
if (!str) return ''
str = str.replace(/^\s+|\s+$/g, '') // trim
str = str.toLowerCase()
// remove accents, swap ñ for n, etc
var from = "àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;"
var to = "aaaaeeeeiiiioooouuuuncescrzyuudtn-----"
for (var i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
}
str = str.replace('.', '-') // replace a dot by a dash
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
.replace(/-+/g, '-') // collapse dashes
.replace(/\//g, '') // collapse all forward-slashes
return str
}
Vue.prototype.$copyToClipboard = (str, ctx) => {
return new Promise((resolve) => {
if (!navigator.clipboard) {
+6
View File
@@ -72,6 +72,9 @@ export const actions = {
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
settingsUpdate.orderBy = 'media.metadata.author'
}
if (state.settings.orderBy == 'media.duration') {
settingsUpdate.orderBy = 'media.numTracks'
}
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
if (invalidFilters.includes(filterByFirstPart)) {
@@ -81,6 +84,9 @@ export const actions = {
if (state.settings.orderBy == 'media.metadata.author') {
settingsUpdate.orderBy = 'media.metadata.authorName'
}
if (state.settings.orderBy == 'media.numTracks') {
settingsUpdate.orderBy = 'media.duration'
}
}
if (Object.keys(settingsUpdate).length) {
dispatch('updateUserSettings', settingsUpdate)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.0.9",
"version": "2.0.10",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
@@ -8,7 +8,7 @@
"start": "node index.js",
"client": "cd client && npm install && npm run generate",
"prod": "npm run client && npm install && node prod.js",
"build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
"build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf .",
"build-linux": "build/linuxpackager",
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
"deploy": "node dist/autodeploy"
+8 -8
View File
@@ -4,16 +4,16 @@ class BackupController {
constructor() { }
async create(req, res) {
if (!req.user.isRoot) {
Logger.error(`[BackupController] Non-Root user attempting to craete backup`, req.user)
if (!req.user.isAdminOrUp) {
Logger.error(`[BackupController] Non-admin user attempting to craete backup`, req.user)
return res.sendStatus(403)
}
this.backupManager.requestCreateBackup(res)
}
async delete(req, res) {
if (!req.user.isRoot) {
Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
if (!req.user.isAdminOrUp) {
Logger.error(`[BackupController] Non-admin user attempting to delete backup`, req.user)
return res.sendStatus(403)
}
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
@@ -25,8 +25,8 @@ class BackupController {
}
async upload(req, res) {
if (!req.user.isRoot) {
Logger.error(`[BackupController] Non-Root user attempting to upload backup`, req.user)
if (!req.user.isAdminOrUp) {
Logger.error(`[BackupController] Non-admin user attempting to upload backup`, req.user)
return res.sendStatus(403)
}
if (!req.files.file) {
@@ -37,8 +37,8 @@ class BackupController {
}
async apply(req, res) {
if (!req.user.isRoot) {
Logger.error(`[BackupController] Non-Root user attempting to apply backup`, req.user)
if (!req.user.isAdminOrUp) {
Logger.error(`[BackupController] Non-admin user attempting to apply backup`, req.user)
return res.sendStatus(403)
}
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
+3 -3
View File
@@ -320,7 +320,7 @@ class LibraryController {
// PATCH: Change the order of libraries
async reorder(req, res) {
if (!req.user.isRoot) {
if (!req.user.isAdminOrUp) {
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
return res.sendStatus(403)
}
@@ -457,7 +457,7 @@ class LibraryController {
}
async matchAll(req, res) {
if (!req.user.isRoot) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
return res.sendStatus(403)
}
@@ -467,7 +467,7 @@ class LibraryController {
// GET: api/scan (Root)
async scan(req, res) {
if (!req.user.isRoot) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user)
return res.sendStatus(403)
}
+6 -6
View File
@@ -331,8 +331,8 @@ class LibraryItemController {
// DELETE: api/items/all
async deleteAll(req, res) {
if (!req.user.isRoot) {
Logger.warn('User other than root attempted to delete all library items', req.user)
if (!req.user.isAdminOrUp) {
Logger.warn('User other than admin attempted to delete all library items', req.user)
return res.sendStatus(403)
}
Logger.info('Removing all Library Items')
@@ -341,10 +341,10 @@ class LibraryItemController {
else res.sendStatus(500)
}
// GET: api/items/:id/scan (Root)
// GET: api/items/:id/scan (admin)
async scan(req, res) {
if (!req.user.isRoot) {
Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user)
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user)
return res.sendStatus(403)
}
@@ -361,7 +361,7 @@ class LibraryItemController {
// POST: api/items/:id/audio-metadata
async updateAudioFileMetadata(req, res) {
if (!req.user.isRoot) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
return res.sendStatus(403)
}
+7 -7
View File
@@ -159,10 +159,10 @@ class MiscController {
res.json(downloads)
}
// PATCH: api/settings (Root)
// PATCH: api/settings (admin)
async updateServerSettings(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to update server settings', req.user)
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to update server settings', req.user)
return res.sendStatus(403)
}
var settingsUpdate = req.body
@@ -185,9 +185,9 @@ class MiscController {
})
}
// POST: api/purgecache (Root)
// POST: api/purgecache (admin)
async purgeCache(req, res) {
if (!req.user.isRoot) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
Logger.info(`[ApiRouter] Purging all cache`)
@@ -239,8 +239,8 @@ class MiscController {
}
getAllTags(req, res) {
if (!req.user.isRoot) {
Logger.error(`[MiscController] Non-root user attempted to getAllTags`)
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
return res.sendStatus(404)
}
var tags = []
+6
View File
@@ -173,6 +173,12 @@ class PodcastController {
}
const feedData = this.rssFeedManager.openPodcastFeed(req.user, req.libraryItem, req.body)
if (feedData.error) {
return res.json({
success: false,
error: feedData.error
})
}
res.json({
success: true,
+19 -13
View File
@@ -7,14 +7,15 @@ class UserController {
constructor() { }
findAll(req, res) {
if (!req.user.isRoot) return res.sendStatus(403)
var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u))
if (!req.user.isAdminOrUp) return res.sendStatus(403)
const hideRootToken = !req.user.isRoot
var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u, hideRootToken))
res.json(users)
}
findOne(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to get user', req.user)
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user)
return res.sendStatus(403)
}
@@ -23,12 +24,12 @@ class UserController {
return res.sendStatus(404)
}
res.json(this.userJsonWithItemProgressDetails(user))
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
}
async create(req, res) {
if (!req.user.isRoot) {
Logger.warn('Non-root user attempted to create user', req.user)
if (!req.user.isAdminOrUp) {
Logger.warn('Non-admin user attempted to create user', req.user)
return res.sendStatus(403)
}
var account = req.body
@@ -57,8 +58,8 @@ class UserController {
}
async update(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to update user', req.user)
if (!req.user.isAdminOrUp) {
Logger.error('[UserController] User other than admin attempting to update user', req.user)
return res.sendStatus(403)
}
@@ -67,6 +68,11 @@ class UserController {
return res.sendStatus(404)
}
if (user.type === 'root' && !req.user.isRoot) {
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
return res.sendStatus(403)
}
var account = req.body
if (account.username !== undefined && account.username !== user.username) {
@@ -95,8 +101,8 @@ class UserController {
}
async delete(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to delete user', req.user)
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') {
@@ -133,7 +139,7 @@ class UserController {
// GET: api/users/:id/listening-sessions
async getListeningSessions(req, res) {
if (!req.user.isRoot && req.user.id !== req.params.id) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403)
}
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
@@ -142,7 +148,7 @@ class UserController {
// GET: api/users/:id/listening-stats
async getListeningStats(req, res) {
if (!req.user.isRoot && req.user.id !== req.params.id) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403)
}
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
+15 -2
View File
@@ -131,8 +131,21 @@ class BackupManager {
var filename = filesInDir[i]
if (filename.endsWith('.audiobookshelf')) {
var fullFilePath = Path.join(this.BackupPath, filename)
const zip = new StreamZip.async({ file: fullFilePath })
const data = await zip.entryData('details')
let zip = null
let data = null
try {
zip = new StreamZip.async({ file: fullFilePath })
data = await zip.entryData('details')
} catch (error) {
if (error.message === "Bad archive") {
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
continue;
} else {
throw error
}
}
var details = data.toString('utf8').split('\n')
var backup = new Backup({ details, fullPath: fullFilePath })
+17 -8
View File
@@ -59,23 +59,23 @@ class RssFeedManager {
readStream.pipe(res)
}
openFeed(userId, feedId, libraryItem, serverAddress) {
openFeed(userId, slug, libraryItem, serverAddress) {
const podcast = libraryItem.media
const feedUrl = `${serverAddress}/feed/${feedId}`
const feedUrl = `${serverAddress}/feed/${slug}`
// Removed Podcast npm package and ip package
const feed = new Podcast({
title: podcast.metadata.title,
description: podcast.metadata.description,
feedUrl,
siteUrl: serverAddress,
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${feedId}/cover` : `${serverAddress}/Logo.png`,
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`,
author: podcast.metadata.author || 'advplyr',
language: 'en'
})
podcast.episodes.forEach((episode) => {
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${feedId}/item`)
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
feed.addItem({
title: episode.title,
@@ -92,7 +92,8 @@ class RssFeedManager {
})
const feedData = {
id: feedId,
id: slug,
slug,
userId,
libraryItemId: libraryItem.id,
libraryItemPath: libraryItem.path,
@@ -101,14 +102,22 @@ class RssFeedManager {
feedUrl,
feed
}
this.feeds[feedId] = feedData
this.feeds[slug] = feedData
return feedData
}
openPodcastFeed(user, libraryItem, options) {
const serverAddress = options.serverAddress
const feedId = getId('feed')
const feedData = this.openFeed(user.id, feedId, libraryItem, serverAddress)
const slug = options.slug
if (this.feeds[slug]) {
Logger.error(`[RssFeedManager] Slug already in use`)
return {
error: `Slug "${slug}" already in use`
}
}
const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress)
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
return feedData
+4 -1
View File
@@ -239,8 +239,11 @@ class ApiRouter {
//
// Helper Methods
//
userJsonWithItemProgressDetails(user) {
userJsonWithItemProgressDetails(user, hideRootToken = false) {
var json = user.toJSONForBrowser()
if (json.type === 'root' && hideRootToken) {
json.token = ''
}
json.mediaProgress = json.mediaProgress.map(lip => {
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)