Merge branch 'advplyr:master' into master

This commit is contained in:
Keagan Hilliard 2021-11-22 18:40:15 -08:00 committed by GitHub
commit c0a4ec23d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1611 additions and 994 deletions

View File

@ -158,7 +158,7 @@ export default {
} }
}) })
this.$axios this.$axios
.patch(`/api/user/audiobooks`, updateProgressPayloads) .patch(`/api/me/audiobook/batch/update`, updateProgressPayloads)
.then(() => { .then(() => {
this.$toast.success('Batch update success!') this.$toast.success('Batch update success!')
this.$store.commit('setProcessingBatch', false) this.$store.commit('setProcessingBatch', false)
@ -177,7 +177,7 @@ export default {
this.processingBatchDelete = true this.processingBatchDelete = true
this.$store.commit('setProcessingBatch', true) this.$store.commit('setProcessingBatch', true)
this.$axios this.$axios
.$post(`/api/audiobooks/delete`, { .$post(`/api/books/batch/delete`, {
audiobookIds: this.selectedAudiobooks audiobookIds: this.selectedAudiobooks
}) })
.then(() => { .then(() => {

View File

@ -162,7 +162,7 @@ export default {
} }
this.isProcessingReadUpdate = true this.isProcessingReadUpdate = true
this.$axios this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload) .$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
.then(() => { .then(() => {
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)

View File

@ -326,7 +326,7 @@ export default {
} }
this.isProcessingReadUpdate = true this.isProcessingReadUpdate = true
this.$axios this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload) .$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
.then(() => { .then(() => {
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)

View File

@ -131,7 +131,7 @@ export default {
} }
this.isFetching = true this.isFetching = true
var searchResults = await this.$axios.$get(`/api/library/${this.currentLibraryId}/search?q=${value}`).catch((error) => { var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return [] return []
}) })

View File

@ -171,7 +171,7 @@ export default {
this.processing = true this.processing = true
console.log('Calling update', account) console.log('Calling update', account)
this.$axios this.$axios
.$patch(`/api/user/${this.account.id}`, account) .$patch(`/api/users/${this.account.id}`, account)
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {
@ -198,7 +198,7 @@ export default {
var account = { ...this.newUser } var account = { ...this.newUser }
this.processing = true this.processing = true
this.$axios this.$axios
.$post('/api/user', account) .$post('/api/users', account)
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {

View File

@ -94,7 +94,7 @@ export default {
this.processing = true this.processing = true
var collectionName = this.collectionName var collectionName = this.collectionName
this.$axios this.$axios
.$delete(`/api/collection/${this.collection.id}`) .$delete(`/api/collections/${this.collection.id}`)
.then(() => { .then(() => {
this.processing = false this.processing = false
this.show = false this.show = false
@ -122,7 +122,7 @@ export default {
description: this.newCollectionDescription || null description: this.newCollectionDescription || null
} }
this.$axios this.$axios
.$patch(`/api/collection/${this.collection.id}`, collectionUpdate) .$patch(`/api/collections/${this.collection.id}`, collectionUpdate)
.then((collection) => { .then((collection) => {
console.log('Collection Updated', collection) console.log('Collection Updated', collection)
this.processing = false this.processing = false

View File

@ -220,7 +220,7 @@ export default {
async fetchFull() { async fetchFull() {
try { try {
this.processing = true this.processing = true
this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`) this.audiobook = await this.$axios.$get(`/api/books/${this.selectedAudiobookId}`)
this.processing = false this.processing = false
} catch (error) { } catch (error) {
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error) console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)

View File

@ -96,7 +96,7 @@ export default {
this.processing = true this.processing = true
this.$axios this.$axios
.$delete(`/api/collection/${collection.id}/book/${this.selectedAudiobookId}`) .$delete(`/api/collections/${collection.id}/book/${this.selectedAudiobookId}`)
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection) console.log(`Book removed from collection`, updatedCollection)
this.$toast.success('Book removed from collection') this.$toast.success('Book removed from collection')
@ -114,7 +114,7 @@ export default {
this.processing = true this.processing = true
this.$axios this.$axios
.$post(`/api/collection/${collection.id}/book`, { id: this.selectedAudiobookId }) .$post(`/api/collections/${collection.id}/book`, { id: this.selectedAudiobookId })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection) console.log(`Book added to collection`, updatedCollection)
this.$toast.success('Book added to collection') this.$toast.success('Book added to collection')

View File

@ -154,7 +154,7 @@ export default {
var coverPayload = { var coverPayload = {
url: updatePayload.cover url: updatePayload.cover
} }
var success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, coverPayload).catch((error) => { var success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
return false return false
}) })
@ -171,7 +171,7 @@ export default {
var bookUpdatePayload = { var bookUpdatePayload = {
book: updatePayload book: updatePayload
} }
var success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, bookUpdatePayload).catch((error) => { var success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
return false return false
}) })

View File

@ -155,7 +155,7 @@ export default {
form.set('cover', this.selectedFile) form.set('cover', this.selectedFile)
this.$axios this.$axios
.$post(`/api/audiobook/${this.audiobook.id}/cover`, form) .$post(`/api/books/${this.audiobook.id}/cover`, form)
.then((data) => { .then((data) => {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
@ -217,7 +217,7 @@ export default {
// Download cover from url and use // Download cover from url and use
if (cover.startsWith('http:') || cover.startsWith('https:')) { if (cover.startsWith('http:') || cover.startsWith('https:')) {
success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, { url: cover }).catch((error) => { success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, { url: cover }).catch((error) => {
console.error('Failed to download cover from url', error) console.error('Failed to download cover from url', error)
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
@ -231,7 +231,7 @@ export default {
cover: cover cover: cover
} }
} }
success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
@ -266,7 +266,7 @@ export default {
setCover(coverFile) { setCover(coverFile) {
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$patch(`/api/audiobook/${this.audiobook.id}/coverfile`, coverFile) .$patch(`/api/books/${this.audiobook.id}/coverfile`, coverFile)
.then((data) => { .then((data) => {
console.log('response data', data) console.log('response data', data)
if (data && typeof data === 'string') { if (data && typeof data === 'string') {

View File

@ -195,7 +195,7 @@ export default {
tags: this.newTags tags: this.newTags
} }
var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
return false return false
}) })
@ -220,27 +220,11 @@ export default {
this.newTags = this.audiobook.tags || [] this.newTags = this.audiobook.tags || []
}, },
resetProgress() {
if (confirm(`Are you sure you want to reset your progress?`)) {
this.resettingProgress = true
this.$axios
.$delete(`/api/user/audiobook/${this.audiobookId}`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.resettingProgress = false
})
.catch((error) => {
console.error('Progress reset failed', error)
this.resettingProgress = false
})
}
},
deleteAudiobook() { deleteAudiobook() {
if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) { if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) {
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$delete(`/api/audiobook/${this.audiobookId}`) .$delete(`/api/books/${this.audiobookId}`)
.then(() => { .then(() => {
console.log('Audiobook removed') console.log('Audiobook removed')
this.$toast.success('Audiobook Removed') this.$toast.success('Audiobook Removed')

View File

@ -133,7 +133,7 @@ export default {
publisher: true, publisher: true,
publishYear: true, publishYear: true,
series: true, series: true,
volumeNumber: true, volumeNumber: true
} }
} }
}, },
@ -198,7 +198,7 @@ export default {
publisher: true, publisher: true,
publishYear: true, publishYear: true,
series: true, series: true,
volumeNumber: true, volumeNumber: true
} }
if (this.audiobook.id !== this.audiobookId) { if (this.audiobook.id !== this.audiobookId) {
@ -238,7 +238,7 @@ export default {
var coverPayload = { var coverPayload = {
url: updatePayload.cover url: updatePayload.cover
} }
var success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, coverPayload).catch((error) => { var success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
return false return false
}) })
@ -255,7 +255,7 @@ export default {
var bookUpdatePayload = { var bookUpdatePayload = {
book: updatePayload book: updatePayload
} }
var success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, bookUpdatePayload).catch((error) => { var success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
return false return false
}) })

View File

@ -105,7 +105,7 @@ export default {
this.$emit('update:processing', true) this.$emit('update:processing', true)
this.$axios this.$axios
.$patch(`/api/library/${this.library.id}`, newLibraryPayload) .$patch(`/api/libraries/${this.library.id}`, newLibraryPayload)
.then((res) => { .then((res) => {
this.$emit('update:processing', false) this.$emit('update:processing', false)
this.$emit('close') this.$emit('close')
@ -137,7 +137,7 @@ export default {
this.$emit('update:processing', true) this.$emit('update:processing', true)
this.$axios this.$axios
.$post('/api/library', newLibraryPayload) .$post('/api/libraries', newLibraryPayload)
.then((res) => { .then((res) => {
this.$emit('update:processing', false) this.$emit('update:processing', false)
this.$emit('close') this.$emit('close')

View File

@ -72,7 +72,7 @@ export default {
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) { if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
this.isDeleting = true this.isDeleting = true
this.$axios this.$axios
.$delete(`/api/library/${this.library.id}`) .$delete(`/api/libraries/${this.library.id}`)
.then((data) => { .then((data) => {
this.isDeleting = false this.isDeleting = false
if (data.error) { if (data.error) {

View File

@ -68,7 +68,7 @@ export default {
books: this.booksCopy.map((b) => b.id) books: this.booksCopy.map((b) => b.id)
} }
this.$axios this.$axios
.$patch(`/api/collection/${this.collectionId}`, collectionUpdate) .$patch(`/api/collections/${this.collectionId}`, collectionUpdate)
.then((collection) => { .then((collection) => {
console.log('Collection updated', collection) console.log('Collection updated', collection)
}) })

View File

@ -101,7 +101,7 @@ export default {
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) { if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
this.isDeletingUser = true this.isDeletingUser = true
this.$axios this.$axios
.$delete(`/api/user/${user.id}`) .$delete(`/api/users/${user.id}`)
.then((data) => { .then((data) => {
this.isDeletingUser = false this.isDeletingUser = false
if (data.error) { if (data.error) {

View File

@ -140,7 +140,7 @@ export default {
} }
this.isProcessingReadUpdate = true this.isProcessingReadUpdate = true
this.$axios this.$axios
.$patch(`/api/user/audiobook/${this.book.id}`, updatePayload) .$patch(`/api/me/audiobook/${this.book.id}`, updatePayload)
.then(() => { .then(() => {
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.success(`"${this.bookTitle}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) this.$toast.success(`"${this.bookTitle}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
@ -155,7 +155,7 @@ export default {
this.processingRemove = true this.processingRemove = true
this.$axios this.$axios
.$delete(`/api/collection/${this.collectionId}/book/${this.book.id}`) .$delete(`/api/collections/${this.collectionId}/book/${this.book.id}`)
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection) console.log(`Book removed from collection`, updatedCollection)
this.$toast.success('Book removed from collection') this.$toast.success('Book removed from collection')

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.6.23", "version": "1.6.26",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -90,7 +90,7 @@ export default {
} }
this.changingPassword = true this.changingPassword = true
this.$axios this.$axios
.$patch('/api/user/password', { .$patch('/api/me/password', {
password: this.password, password: this.password,
newPassword: this.newPassword newPassword: this.newPassword
}) })

View File

@ -115,7 +115,7 @@ export default {
if (!store.getters['user/getUserCanUpdate']) { if (!store.getters['user/getUserCanUpdate']) {
return redirect('/?error=unauthorized') return redirect('/?error=unauthorized')
} }
var audiobook = await app.$axios.$get(`/api/audiobook/${params.id}`).catch((error) => { var audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false
}) })
@ -291,7 +291,7 @@ export default {
this.saving = true this.saving = true
this.$axios this.$axios
.$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { orderedFileData }) .$patch(`/api/books/${this.audiobook.id}/tracks`, { orderedFileData })
.then((data) => { .then((data) => {
console.log('Finished patching files', data) console.log('Finished patching files', data)
this.saving = false this.saving = false

View File

@ -161,7 +161,7 @@ export default {
if (!store.state.user.user) { if (!store.state.user.user) {
return redirect(`/login?redirect=${route.path}`) return redirect(`/login?redirect=${route.path}`)
} }
var audiobook = await app.$axios.$get(`/api/audiobook/${params.id}`).catch((error) => { var audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false
}) })
@ -383,7 +383,7 @@ export default {
} }
this.isProcessingReadUpdate = true this.isProcessingReadUpdate = true
this.$axios this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload) .$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
.then(() => { .then(() => {
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`) this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
@ -417,7 +417,7 @@ export default {
audiobookUpdated() { audiobookUpdated() {
console.log('Audiobook Updated - Fetch full audiobook') console.log('Audiobook Updated - Fetch full audiobook')
this.$axios this.$axios
.$get(`/api/audiobook/${this.audiobookId}`) .$get(`/api/books/${this.audiobookId}`)
.then((audiobook) => { .then((audiobook) => {
console.log('Updated audiobook', audiobook) console.log('Updated audiobook', audiobook)
this.audiobook = audiobook this.audiobook = audiobook
@ -430,7 +430,7 @@ export default {
if (confirm(`Are you sure you want to reset your progress?`)) { if (confirm(`Are you sure you want to reset your progress?`)) {
this.resettingProgress = true this.resettingProgress = true
this.$axios this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`) .$patch(`/api/me/audiobook/${this.audiobookId}/reset-progress`)
.then(() => { .then(() => {
console.log('Progress reset complete') console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`) this.$toast.success(`Your progress was reset`)

View File

@ -169,7 +169,7 @@ export default {
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$post('/api/audiobooks/update', this.audiobookCopies) .$post('/api/books/batch/update', this.audiobookCopies)
.then((data) => { .then((data) => {
this.isProcessing = false this.isProcessing = false
if (data.updates) { if (data.updates) {

View File

@ -44,7 +44,7 @@ export default {
if (!store.state.user.user) { if (!store.state.user.user) {
return redirect(`/login?redirect=${route.path}`) return redirect(`/login?redirect=${route.path}`)
} }
var collection = await app.$axios.$get(`/api/collection/${params.id}`).catch((error) => { var collection = await app.$axios.$get(`/api/collections/${params.id}`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false
}) })
@ -105,7 +105,7 @@ export default {
this.processingRemove = true this.processingRemove = true
var collectionName = this.collectionName var collectionName = this.collectionName
this.$axios this.$axios
.$delete(`/api/collection/${this.collection.id}`) .$delete(`/api/collections/${this.collection.id}`)
.then(() => { .then(() => {
this.processingRemove = false this.processingRemove = false
this.$toast.success(`Collection "${collectionName}" Removed`) this.$toast.success(`Collection "${collectionName}" Removed`)

View File

@ -150,7 +150,7 @@ export default {
if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) { if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
this.isResettingAudiobooks = true this.isResettingAudiobooks = true
this.$axios this.$axios
.$delete('/api/audiobooks') .$delete('/api/books/all')
.then(() => { .then(() => {
this.isResettingAudiobooks = false this.isResettingAudiobooks = false
this.$toast.success('Successfully reset audiobooks') this.$toast.success('Successfully reset audiobooks')

View File

@ -97,7 +97,7 @@ export default {
}, },
methods: { methods: {
async init() { async init() {
this.listeningStats = await this.$axios.$get(`/api/user/${this.user.id}/listeningStats`).catch((err) => { this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []
}) })

View File

@ -71,7 +71,7 @@
<script> <script>
export default { export default {
async asyncData({ params, redirect, app }) { async asyncData({ params, redirect, app }) {
var user = await app.$axios.$get(`/api/user/${params.id}`).catch((error) => { var user = await app.$axios.$get(`/api/users/${params.id}`).catch((error) => {
console.error('Failed to get user', error) console.error('Failed to get user', error)
return null return null
}) })
@ -115,11 +115,11 @@ export default {
}, },
methods: { methods: {
async init() { async init() {
this.listeningSessions = await this.$axios.$get(`/api/user/${this.user.id}/listeningSessions`).catch((err) => { this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []
}) })
this.listeningStats = await this.$axios.$get(`/api/user/${this.user.id}/listeningStats`).catch((err) => { this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []
}) })

View File

@ -31,7 +31,7 @@ export default {
if (params.id === 'search' && query.query) { if (params.id === 'search' && query.query) {
searchQuery = query.query searchQuery = query.query
searchResults = await app.$axios.$get(`/api/library/${libraryId}/search?q=${searchQuery}`).catch((error) => { searchResults = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${searchQuery}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return {} return {}
}) })
@ -92,7 +92,7 @@ export default {
methods: { methods: {
async newQuery() { async newQuery() {
var query = this.$route.query.query var query = this.$route.query.query
this.searchResults = await this.$axios.$get(`/api/library/${this.libraryId}/search?q=${query}`).catch((error) => { this.searchResults = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${query}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return {} return {}
}) })

View File

@ -211,7 +211,7 @@ export const actions = {
commit('setLoadedLibrary', currentLibraryId) commit('setLoadedLibrary', currentLibraryId)
this.$axios this.$axios
.$get(`/api/library/${currentLibraryId}/audiobooks`) .$get(`/api/libraries/${currentLibraryId}/books`)
.then((data) => { .then((data) => {
commit('set', data) commit('set', data)
commit('setLastLoad') commit('setLastLoad')

View File

@ -60,7 +60,7 @@ export const actions = {
} }
return this.$axios return this.$axios
.$get(`/api/library/${libraryId}`) .$get(`/api/libraries/${libraryId}`)
.then((data) => { .then((data) => {
commit('addUpdate', data) commit('addUpdate', data)
commit('setCurrentLibrary', libraryId) commit('setCurrentLibrary', libraryId)

View File

@ -64,7 +64,7 @@ export const actions = {
} }
// Immediately update // Immediately update
commit('setSettings', updatePayload) commit('setSettings', updatePayload)
return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => { return this.$axios.$patch('/api/me/settings', updatePayload).then((result) => {
if (result.success) { if (result.success) {
commit('setSettings', result.settings) commit('setSettings', result.settings)
return true return true

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.6.23", "version": "1.6.26",
"description": "Self-hosted audiobook server for managing and playing audiobooks", "description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -50,4 +50,4 @@
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": {} "devDependencies": {}
} }

File diff suppressed because it is too large Load Diff

View File

@ -355,9 +355,6 @@ class Scanner {
Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`) Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
} }
// var audiobooksInLibrary = this.audiobooks.filter(ab => ab.libraryId === libraryId)
// var existingAudiobook = audiobooksInLibrary.find(a => a.ino === audiobookData.ino)
// inode value may change when using shared drives, update inode if matching path is found // inode value may change when using shared drives, update inode if matching path is found
// Note: inode will not change on rename // Note: inode will not change on rename
var hasUpdatedIno = false var hasUpdatedIno = false
@ -457,7 +454,6 @@ class Scanner {
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos" // TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
// TEMP - update ino for each audiobook
if (audiobooksInLibrary.length) { if (audiobooksInLibrary.length) {
for (let i = 0; i < audiobooksInLibrary.length; i++) { for (let i = 0; i < audiobooksInLibrary.length; i++) {
var ab = audiobooksInLibrary[i] var ab = audiobooksInLibrary[i]
@ -466,7 +462,7 @@ class Scanner {
if (shouldUpdateIno) { if (shouldUpdateIno) {
var filesWithMissingIno = ab.getFilesWithMissingIno() var filesWithMissingIno = ab.getFilesWithMissingIno()
Logger.debug(`\n\Updating inos for "${ab.title}"`) Logger.debug(`\nUpdating inos for "${ab.title}"`)
Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno) Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno)
var hasUpdates = await ab.checkUpdateInos() var hasUpdates = await ab.checkUpdateInos()
@ -507,7 +503,7 @@ class Scanner {
// Check for removed audiobooks // Check for removed audiobooks
for (let i = 0; i < audiobooksInLibrary.length; i++) { for (let i = 0; i < audiobooksInLibrary.length; i++) {
var audiobook = audiobooksInLibrary[i] var audiobook = audiobooksInLibrary[i]
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino) var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
if (!dataFound) { if (!dataFound) {
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
audiobook.isMissing = true audiobook.isMissing = true

View File

@ -0,0 +1,31 @@
const Logger = require('../Logger')
class BackupController {
constructor() { }
async delete(req, res) {
if (!req.user.isRoot) {
Logger.error(`[ApiController] Non-Root user attempting to delete backup`, req.user)
return res.sendStatus(403)
}
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
if (!backup) {
return res.sendStatus(404)
}
await this.backupManager.removeBackup(backup)
res.json(this.backupManager.backups.map(b => b.toJSON()))
}
async upload(req, res) {
if (!req.user.isRoot) {
Logger.error(`[ApiController] Non-Root user attempting to upload backup`, req.user)
return res.sendStatus(403)
}
if (!req.files.file) {
Logger.error('[ApiController] Upload backup invalid')
return res.sendStatus(500)
}
this.backupManager.uploadBackup(req, res)
}
}
module.exports = new BackupController()

View File

@ -0,0 +1,220 @@
const Logger = require('../Logger')
class BookController {
constructor(db, emitter, clientEmitter, streamManager, coverController) {
this.db = db
this.emitter = emitter
this.clientEmitter = clientEmitter
this.streamManager = streamManager
this.coverController = coverController
}
findAll(req, res) {
var audiobooks = []
if (req.query.q) {
audiobooks = this.db.audiobooks.filter(ab => {
return ab.isSearchMatch(req.query.q)
}).map(ab => ab.toJSONMinified())
} else {
audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
}
res.json(audiobooks)
}
findOne(req, res) {
if (!req.user) {
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
// Check user can access this audiobooks library
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
return res.sendStatus(403)
}
res.json(audiobook.toJSONExpanded())
}
async update(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to update without permission', req.user)
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var hasUpdates = audiobook.update(req.body)
if (hasUpdates) {
await this.db.updateAudiobook(audiobook)
}
this.emitter('audiobook_updated', audiobook.toJSONMinified())
res.json(audiobook.toJSON())
}
async delete(req, res) {
if (!req.user.canDelete) {
Logger.warn('User attempted to delete without permission', req.user)
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
await this.handleDeleteAudiobook(audiobook)
res.sendStatus(200)
}
// DELETE: api/books/all
async deleteAll(req, res) {
if (!req.user.isRoot) {
Logger.warn('User other than root attempted to delete all audiobooks', req.user)
return res.sendStatus(403)
}
Logger.info('Removing all Audiobooks')
var success = await this.db.recreateAudiobookDb()
if (success) res.sendStatus(200)
else res.sendStatus(500)
}
// POST: api/books/batch/delete
async batchDelete(req, res) {
if (!req.user.canDelete) {
Logger.warn('User attempted to delete without permission', req.user)
return res.sendStatus(403)
}
var { audiobookIds } = req.body
if (!audiobookIds || !audiobookIds.length) {
return res.sendStatus(500)
}
var audiobooksToDelete = this.db.audiobooks.filter(ab => audiobookIds.includes(ab.id))
if (!audiobooksToDelete.length) {
return res.sendStatus(404)
}
for (let i = 0; i < audiobooksToDelete.length; i++) {
Logger.info(`[ApiController] Deleting Audiobook "${audiobooksToDelete[i].title}"`)
await this.handleDeleteAudiobook(audiobooksToDelete[i])
}
res.sendStatus(200)
}
// POST: api/books/batch/update
async batchUpdate(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to batch update without permission', req.user)
return res.sendStatus(403)
}
var audiobooks = req.body
if (!audiobooks || !audiobooks.length) {
return res.sendStatus(500)
}
var audiobooksUpdated = 0
audiobooks = audiobooks.map((ab) => {
var _ab = this.db.audiobooks.find(__ab => __ab.id === ab.id)
if (!_ab) return null
var hasUpdated = _ab.update(ab)
if (!hasUpdated) return null
audiobooksUpdated++
return _ab
}).filter(ab => ab)
if (audiobooksUpdated) {
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
for (let i = 0; i < audiobooks.length; i++) {
await this.db.updateAudiobook(audiobooks[i])
this.emitter('audiobook_updated', audiobooks[i].toJSONMinified())
}
}
res.json({
success: true,
updates: audiobooksUpdated
})
}
// PATCH: api/books/:id/tracks
async updateTracks(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to update audiotracks without permission', req.user)
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var orderedFileData = req.body.orderedFileData
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
audiobook.updateAudioTracks(orderedFileData)
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONMinified())
res.json(audiobook.toJSON())
}
// GET: api/books/:id/stream
openStream(req, res) {
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
this.streamManager.openStreamApiRequest(res, req.user, audiobook)
}
// POST: api/books/:id/cover
async uploadCover(req, res) {
if (!req.user.canUpload || !req.user.canUpdate) {
Logger.warn('User attempted to upload a cover without permission', req.user)
return res.sendStatus(403)
}
var audiobookId = req.params.id
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
if (!audiobook) {
return res.status(404).send('Audiobook not found')
}
var result = null
if (req.body && req.body.url) {
Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`)
result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url)
} else if (req.files && req.files.cover) {
Logger.debug(`[ApiController] Handling uploaded cover`)
var coverFile = req.files.cover
result = await this.coverController.uploadCover(audiobook, coverFile)
} else {
return res.status(400).send('Invalid request no file or url')
}
if (result && result.error) {
return res.status(400).send(result.error)
} else if (!result || !result.cover) {
return res.status(500).send('Unknown error occurred')
}
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONMinified())
res.json({
success: true,
cover: result.cover
})
}
// PATCH api/books/:id/coverfile
async updateCoverFromFile(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to update without permission', req.user)
return res.sendStatus(403)
}
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var coverFile = req.body
var updated = await audiobook.setCoverFromFile(coverFile)
if (updated) {
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONMinified())
}
if (updated) res.status(200).send('Cover updated successfully')
else res.status(200).send('No update was made to cover')
}
}
module.exports = new BookController()

View File

@ -0,0 +1,97 @@
const Logger = require('../Logger')
const UserCollection = require('../objects/UserCollection')
class CollectionController {
constructor() { }
async create(req, res) {
var newCollection = new UserCollection()
req.body.userId = req.user.id
var success = newCollection.setData(req.body)
if (!success) {
return res.status(500).send('Invalid collection data')
}
var jsonExpanded = newCollection.toJSONExpanded(this.db.audiobooks)
await this.db.insertEntity('collection', newCollection)
this.clientEmitter(req.user.id, 'collection_added', jsonExpanded)
res.json(jsonExpanded)
}
findAll(req, res) {
var collections = this.db.collections.filter(c => c.userId === req.user.id)
var expandedCollections = collections.map(c => c.toJSONExpanded(this.db.audiobooks))
res.json(expandedCollections)
}
findOne(req, res) {
var collection = this.db.collections.find(c => c.id === req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
res.json(collection.toJSONExpanded(this.db.audiobooks))
}
async update(req, res) {
var collection = this.db.collections.find(c => c.id === req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
var wasUpdated = collection.update(req.body)
var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
if (wasUpdated) {
await this.db.updateEntity('collection', collection)
this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
}
res.json(jsonExpanded)
}
async delete(req, res) {
var collection = this.db.collections.find(c => c.id === req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
await this.db.removeEntity('collection', collection.id)
this.clientEmitter(req.user.id, 'collection_removed', jsonExpanded)
res.sendStatus(200)
}
async addBook(req, res) {
var collection = this.db.collections.find(c => c.id === req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
var audiobook = this.db.audiobooks.find(ab => ab.id === req.body.id)
if (!audiobook) {
return res.status(500).send('Book not found')
}
if (audiobook.libraryId !== collection.libraryId) {
return res.status(500).send('Book in different library')
}
if (collection.books.includes(req.body.id)) {
return res.status(500).send('Book already in collection')
}
collection.addBook(req.body.id)
var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
await this.db.updateEntity('collection', collection)
this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
res.json(jsonExpanded)
}
// DELETE: api/collections/:id/book/:bookId
async removeBook(req, res) {
var collection = this.db.collections.find(c => c.id === req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}
if (collection.books.includes(req.params.bookId)) {
collection.removeBook(req.params.bookId)
var jsonExpanded = collection.toJSONExpanded(this.db.audiobooks)
await this.db.updateEntity('collection', collection)
this.clientEmitter(req.user.id, 'collection_updated', jsonExpanded)
}
res.json(collection.toJSONExpanded(this.db.audiobooks))
}
}
module.exports = new CollectionController()

View File

@ -0,0 +1,205 @@
const Logger = require('../Logger')
const Library = require('../objects/Library')
class LibraryController {
constructor() { }
async create(req, res) {
var newLibraryPayload = {
...req.body
}
if (!newLibraryPayload.name || !newLibraryPayload.folders || !newLibraryPayload.folders.length) {
return res.status(500).send('Invalid request')
}
var library = new Library()
newLibraryPayload.displayOrder = this.db.libraries.length + 1
library.setData(newLibraryPayload)
await this.db.insertEntity('library', library)
this.emitter('library_added', library.toJSON())
// Add library watcher
this.watcher.addLibrary(library)
res.json(library)
}
findAll(req, res) {
res.json(this.db.libraries.map(lib => lib.toJSON()))
}
findOne(req, res) {
if (!req.params.id) return res.status(500).send('Invalid id parameter')
var library = this.db.libraries.find(lib => lib.id === req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
return res.json(library.toJSON())
}
async update(req, res) {
var library = this.db.libraries.find(lib => lib.id === req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
var hasUpdates = library.update(req.body)
if (hasUpdates) {
// Update watcher
this.watcher.updateLibrary(library)
// Remove audiobooks no longer in library
var audiobooksToRemove = this.db.audiobooks.filter(ab => !library.checkFullPathInLibrary(ab.fullPath))
if (audiobooksToRemove.length) {
Logger.info(`[Scanner] Updating library, removing ${audiobooksToRemove.length} audiobooks`)
for (let i = 0; i < audiobooksToRemove.length; i++) {
await this.handleDeleteAudiobook(audiobooksToRemove[i])
}
}
await this.db.updateEntity('library', library)
this.emitter('library_updated', library.toJSON())
}
return res.json(library.toJSON())
}
async delete(req, res) {
var library = this.db.libraries.find(lib => lib.id === req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
// Remove library watcher
this.watcher.removeLibrary(library)
// Remove audiobooks in this library
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
Logger.info(`[Server] deleting library "${library.name}" with ${audiobooks.length} audiobooks"`)
for (let i = 0; i < audiobooks.length; i++) {
await this.handleDeleteAudiobook(audiobooks[i])
}
var libraryJson = library.toJSON()
await this.db.removeEntity('library', library.id)
this.emitter('library_removed', libraryJson)
return res.json(libraryJson)
}
// api/libraries/:id/books
getBooksForLibrary(req, res) {
var libraryId = req.params.id
var library = this.db.libraries.find(lib => lib.id === libraryId)
if (!library) {
return res.status(400).send('Library does not exist')
}
var audiobooks = []
if (req.query.q) {
audiobooks = this.db.audiobooks.filter(ab => {
return ab.libraryId === libraryId && ab.isSearchMatch(req.query.q)
}).map(ab => ab.toJSONMinified())
} else {
audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId).map(ab => ab.toJSONMinified())
}
res.json(audiobooks)
}
// PATCH: Change the order of libraries
async reorder(req, res) {
if (!req.user.isRoot) {
Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
return res.sendStatus(401)
}
var orderdata = req.body
var hasUpdates = false
for (let i = 0; i < orderdata.length; i++) {
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
if (!library) {
Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
return res.sendStatus(500)
}
if (library.update({ displayOrder: orderdata[i].newOrder })) {
hasUpdates = true
await this.db.updateEntity('library', library)
}
}
if (hasUpdates) {
Logger.info(`[ApiController] Updated library display orders`)
} else {
Logger.info(`[ApiController] Library orders were up to date`)
}
var libraries = this.db.libraries.map(lib => lib.toJSON())
res.json(libraries)
}
// GET: Global library search
search(req, res) {
var library = this.db.libraries.find(lib => lib.id === req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}
if (!req.query.q) {
return res.status(400).send('No query string')
}
var maxResults = req.query.max || 3
var bookMatches = []
var authorMatches = {}
var seriesMatches = {}
var tagMatches = {}
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
audiobooksInLibrary.forEach((ab) => {
var queryResult = ab.searchQuery(req.query.q)
if (queryResult.book) {
var bookMatchObj = {
audiobook: ab,
matchKey: queryResult.book,
matchText: queryResult.bookMatchText
}
bookMatches.push(bookMatchObj)
}
if (queryResult.authors) {
queryResult.authors.forEach((author) => {
if (!authorMatches[author]) {
authorMatches[author] = {
author: author
}
}
})
}
if (queryResult.series) {
if (!seriesMatches[queryResult.series]) {
seriesMatches[queryResult.series] = {
series: queryResult.series,
audiobooks: [ab]
}
} else {
seriesMatches[queryResult.series].audiobooks.push(ab)
}
}
if (queryResult.tags && queryResult.tags.length) {
queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) {
tagMatches[tag] = {
tag,
audiobooks: [ab]
}
} else {
tagMatches[tag].audiobooks.push(ab)
}
})
}
})
res.json({
audiobooks: bookMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults)
})
}
}
module.exports = new LibraryController()

View File

@ -0,0 +1,96 @@
const Logger = require('../Logger')
const { isObject } = require('../utils/index')
class MeController {
constructor() { }
// GET: api/me/listening-sessions
async getListeningSessions(req, res) {
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
res.json(listeningSessions.slice(0, 10))
}
// GET: api/me/listening-stats
async getListeningStats(req, res) {
var listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
res.json(listeningStats)
}
// PATCH: api/me/audiobook/:id/reset-progress
async resetAudiobookProgress(req, res) {
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
if (!audiobook) {
return res.status(404).send('Audiobook not found')
}
req.user.resetAudiobookProgress(audiobook)
await this.db.updateEntity('user', req.user)
var userAudiobookData = req.user.audiobooks[audiobook.id]
if (userAudiobookData) {
this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: audiobook.id, data: userAudiobookData })
}
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200)
}
// PATCH: api/me/audiobook/:id
async updateAudiobookData(req, res) {
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
if (!audiobook) {
return res.status(404).send('Audiobook not found')
}
var wasUpdated = req.user.updateAudiobookData(audiobook, req.body)
if (wasUpdated) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
}
// PATCH: api/me/audiobook/batch/update
async batchUpdateAudiobookData(req, res) {
var userAbDataPayloads = req.body
if (!userAbDataPayloads || !userAbDataPayloads.length) {
return res.sendStatus(500)
}
var shouldUpdate = false
userAbDataPayloads.forEach((userAbData) => {
var audiobook = this.db.audiobooks.find(ab => ab.id === userAbData.audiobookId)
if (audiobook) {
var wasUpdated = req.user.updateAudiobookData(audiobook, userAbData)
if (wasUpdated) shouldUpdate = true
}
})
if (shouldUpdate) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
}
// PATCH: api/me/password
updatePassword(req, res) {
this.auth.userChangePassword(req, res)
}
// PATCH: api/me/settings
async updateSettings(req, res) {
var settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
return res.sendStatus(500)
}
var madeUpdates = req.user.updateSettings(settingsUpdate)
if (madeUpdates) {
await this.db.updateEntity('user', req.user)
}
return res.json({
success: true,
settings: req.user.settings
})
}
}
module.exports = new MeController()

View File

@ -0,0 +1,152 @@
const Logger = require('../Logger')
const User = require('../objects/User')
const { getId } = require('../utils/index')
class UserController {
constructor() { }
async create(req, res) {
if (!req.user.isRoot) {
Logger.warn('Non-root user attempted to create user', req.user)
return res.sendStatus(403)
}
var account = req.body
var username = account.username
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
if (usernameExists) {
return res.status(500).send('Username already taken')
}
account.id = getId('usr')
account.pash = await this.auth.hashPass(account.password)
delete account.password
account.token = await this.auth.generateAccessToken({ userId: account.id })
account.createdAt = Date.now()
var newUser = new User(account)
var success = await this.db.insertEntity('user', newUser)
if (success) {
this.clientEmitter(req.user.id, 'user_added', newUser)
res.json({
user: newUser.toJSONForBrowser()
})
} else {
return res.status(500).send('Failed to save new user')
}
}
findAll(req, res) {
if (!req.user.isRoot) return res.sendStatus(403)
var users = this.db.users.map(u => this.userJsonWithBookProgressDetails(u))
res.json(users)
}
findOne(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to get 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)
}
res.json(this.userJsonWithBookProgressDetails(user))
}
async update(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root 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)
}
var account = req.body
if (account.username !== undefined && account.username !== user.username) {
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
if (usernameExists) {
return res.status(500).send('Username already taken')
}
}
// Updating password
if (account.password) {
account.pash = await this.auth.hashPass(account.password)
delete account.password
}
var hasUpdated = user.update(account)
if (hasUpdated) {
await this.db.updateEntity('user', user)
}
this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
res.json({
success: true,
user: user.toJSONForBrowser()
})
}
async delete(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to delete user', req.user)
return res.sendStatus(403)
}
if (req.params.id === 'root') {
return res.sendStatus(500)
}
if (req.user.id === req.params.id) {
Logger.error('Attempting to delete themselves...')
return res.sendStatus(500)
}
var user = this.db.users.find(u => u.id === req.params.id)
if (!user) {
Logger.error('User not found')
return res.json({
error: 'User not found'
})
}
// delete user collections
var userCollections = this.db.collections.filter(c => c.userId === user.id)
var collectionsToRemove = userCollections.map(uc => uc.id)
for (let i = 0; i < collectionsToRemove.length; i++) {
await this.db.removeEntity('collection', collectionsToRemove[i])
}
// Todo: check if user is logged in and cancel streams
var userJson = user.toJSONForBrowser()
await this.db.removeEntity('user', user.id)
this.clientEmitter(req.user.id, 'user_removed', userJson)
res.json({
success: true
})
}
// GET: api/users/:id/listening-sessions
async getListeningSessions(req, res) {
if (!req.user.isRoot && req.user.id !== req.params.id) {
return res.sendStatus(403)
}
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
res.json(listeningSessions.slice(0, 10))
}
// GET: api/users/:id/listening-stats
async getListeningStats(req, res) {
if (!req.user.isRoot && req.user.id !== req.params.id) {
return res.sendStatus(403)
}
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
res.json(listeningStats)
}
}
module.exports = new UserController()

View File

@ -153,6 +153,17 @@ class AudioFile {
this.metadata.setData(data) this.metadata.setData(data)
} }
// New scanner creates AudioFile from AudioFileScanner
setData2(fileData, probeData) {
this.index = fileData.index || null
this.ino = fileData.ino || null
this.filename = fileData.filename
this.ext = fileData.ext
this.path = fileData.path
this.fullPath = fileData.fullPath
this.addedAt = Date.now()
}
syncChapters(updatedChapters) { syncChapters(updatedChapters) {
if (this.chapters.length !== updatedChapters.length) { if (this.chapters.length !== updatedChapters.length) {
this.chapters = updatedChapters.map(ch => ({ ...ch })) this.chapters = updatedChapters.map(ch => ({ ...ch }))

View File

@ -353,6 +353,7 @@ class Audiobook {
if (imageFile) { if (imageFile) {
data.coverFullPath = imageFile.fullPath data.coverFullPath = imageFile.fullPath
var relImagePath = imageFile.path.replace(this.path, '') var relImagePath = imageFile.path.replace(this.path, '')
console.log('SET BOOK PATH', imageFile.path, 'REPLACE', this.path, 'RESULT', relImagePath)
data.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath) data.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath)
} }
} }
@ -822,5 +823,141 @@ class Audiobook {
var audioFile = this.audioFiles[0] var audioFile = this.audioFiles[0]
return this.book.setDetailsFromFileMetadata(audioFile.metadata) return this.book.setDetailsFromFileMetadata(audioFile.metadata)
} }
// Returns null if file not found, true if file was updated, false if up to date
checkFileFound(fileFound, isAudioFile) {
var hasUpdated = false
const arrayToCheck = isAudioFile ? this.audioFiles : this.otherFiles
var existingFile = arrayToCheck.find(_af => _af.ino === fileFound.ino)
if (!existingFile) {
existingFile = arrayToCheck.find(_af => _af.path === fileFound.path)
if (existingFile) {
// file inode was updated
existingFile.ino = fileFound.ino
hasUpdated = true
} else {
// file not found
return null
}
}
if (existingFile.filename !== fileFound.filename) {
existingFile.filename = fileFound.filename
existingFile.ext = fileFound.ext
hasUpdated = true
}
if (existingFile.path !== fileFound.path) {
existingFile.path = fileFound.path
existingFile.fullPath = fileFound.fullPath
hasUpdated = true
} else if (existingFile.fullPath !== fileFound.fullPath) {
existingFile.fullPath = fileFound.fullPath
hasUpdated = true
}
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
existingFile.filetype = fileFound.filetype
hasUpdated = true
}
return hasUpdated
}
checkShouldScan(dataFound) {
var hasUpdated = false
if (dataFound.ino !== this.ino) {
this.ino = dataFound.ino
hasUpdated = true
}
if (dataFound.folderId !== this.folderId) {
Logger.warn(`[Audiobook] Check scan audiobook changed folder ${this.folderId} -> ${dataFound.folderId}`)
this.folderId = dataFound.folderId
hasUpdated = true
}
if (dataFound.path !== this.path) {
Logger.warn(`[Audiobook] Check scan audiobook changed path "${this.path}" -> "${dataFound.path}"`)
this.path = dataFound.path
this.fullPath = dataFound.fullPath
hasUpdated = true
} else if (dataFound.fullPath !== this.fullPath) {
Logger.warn(`[Audiobook] Check scan audiobook changed fullpath "${this.fullPath}" -> "${dataFound.fullPath}"`)
this.fullPath = dataFound.fullPath
hasUpdated = true
}
var newAudioFileData = []
var newOtherFileData = []
dataFound.audioFiles.forEach((af) => {
var audioFileFoundCheck = this.checkFileFound(af, true)
if (audioFileFoundCheck === null) {
newAudioFileData.push(af)
} else if (audioFileFoundCheck === true) {
hasUpdated = true
}
})
dataFound.otherFiles.forEach((otherFileData) => {
var fileFoundCheck = this.checkFileFound(otherFileData, false)
if (fileFoundCheck === null) {
newOtherFileData.push(otherFileData)
} else if (fileFoundCheck === true) {
hasUpdated = true
}
})
const audioFilesRemoved = []
const otherFilesRemoved = []
// inodes will all be up to date at this point
this.audioFiles = this.audioFiles.filter(af => {
if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) {
audioFilesRemoved.push(af.toJSON())
return false
}
return true
})
// Remove all tracks that were associated with removed audio files
if (audioFilesRemoved.length) {
const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino)
this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino))
this.checkUpdateMissingParts()
hasUpdated = true
}
this.otherFiles = this.otherFiles.filter(otherFile => {
if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) {
otherFilesRemoved.push(otherFile.toJSON())
// Check remove cover
if (otherFile.fullPath === this.book.coverFullPath) {
Logger.debug(`[Audiobook] "${this.title}" Check scan book cover removed`)
this.book.removeCover()
}
return false
}
return true
})
if (otherFilesRemoved.length) {
hasUpdated = true
}
return {
updated: hasUpdated,
newAudioFileData,
newOtherFileData,
audioFilesRemoved,
otherFilesRemoved
}
}
} }
module.exports = Audiobook module.exports = Audiobook

View File

@ -0,0 +1,22 @@
const AudioFile = require('../objects/AudioFile')
const AudioProbeData = require('./AudioProbeData')
const prober = require('../utils/prober')
const Logger = require('../Logger')
class AudioFileScanner {
constructor() { }
async scan(audioFileData, verbose = false) {
var probeData = await prober.probe2(audioFileData.fullPath, verbose)
if (probeData.error) {
Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioFileData.fullPath}"`)
return null
}
var audioFile = new AudioFile()
// TODO: Build audio file
return audioFile
}
}
module.exports = new AudioFileScanner()

View File

@ -0,0 +1,74 @@
const AudioFileMetadata = require('../objects/AudioFileMetadata')
class AudioProbeData {
constructor() {
this.embeddedCoverArt = null
this.format = null
this.duration = null
this.size = null
this.bitRate = null
this.codec = null
this.timeBase = null
this.language = null
this.channelLayout = null
this.channels = null
this.sampleRate = null
this.chapters = []
this.audioFileMetadata = null
this.trackNumber = null
this.trackTotal = null
}
getDefaultAudioStream(audioStreams) {
if (audioStreams.length === 1) return audioStreams[0]
var defaultStream = audioStreams.find(a => a.is_default)
if (!defaultStream) return audioStreams[0]
return defaultStream
}
getEmbeddedCoverArt(videoStream) {
const ImageCodecs = ['mjpeg', 'jpeg', 'png']
return ImageCodecs.includes(videoStream.codec) ? videoStream.codec : null
}
setData(data) {
var audioStream = getDefaultAudioStream(data.audio_streams)
this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : false
this.format = data.format
this.duration = data.duration
this.size = data.size
this.bitRate = audioStream.bit_rate || data.bit_rate
this.codec = audioStream.codec
this.timeBase = audioStream.time_base
this.language = audioStream.language
this.channelLayout = audioStream.channel_layout
this.channels = audioStream.channels
this.sampleRate = audioStream.sample_rate
this.chapters = data.chapters || []
var metatags = {}
for (const key in data) {
if (data[key] && key.startsWith('file_tag')) {
metatags[key] = data[key]
}
}
this.audioFileMetadata = new AudioFileMetadata()
this.audioFileMetadata.setData(metatags)
// Track ID3 tag might be "3/10" or just "3"
if (this.audioFileMetadata.tagTrack) {
var trackParts = this.audioFileMetadata.tagTrack.split('/').map(part => Number(part))
if (trackParts.length > 0) {
this.trackNumber = trackParts[0]
}
if (trackParts.length > 1) {
this.trackTotal = trackParts[1]
}
}
}
}
module.exports = AudioProbeData

View File

@ -0,0 +1,34 @@
const Folder = require('../objects/Folder')
const { getId } = require('../utils/index')
class LibraryScan {
constructor() {
this.id = null
this.libraryId = null
this.libraryName = null
this.folders = null
this.scanOptions = null
this.startedAt = null
this.finishedAt = null
this.folderScans = []
}
get _scanOptions() { return this.scanOptions || {} }
get forceRescan() { return !!this._scanOptions.forceRescan }
setData(library, scanOptions) {
this.id = getId('lscan')
this.libraryId = library.id
this.libraryName = library.name
this.folders = library.folders.map(folder => Folder(folder.toJSON()))
this.scanOptions = scanOptions
this.startedAt = Date.now()
}
}
module.exports = LibraryScan

View File

@ -0,0 +1,68 @@
const { CoverDestination } = require('../utils/constants')
class ScanOptions {
constructor(options) {
this.forceRescan = false
this.metadataPrecedence = [
{
id: 'directory',
include: true
},
{
id: 'reader-desc-txt',
include: true
},
{
id: 'audio-file-metadata',
include: true
},
{
id: 'metadata-opf',
include: true
},
{
id: 'external-source',
include: false
}
]
// Server settings
this.parseSubtitles = false
this.findCovers = false
this.coverDestination = CoverDestination.METADATA
if (options) {
this.construct(options)
}
}
construct(options) {
for (const key in options) {
if (key === 'metadataPrecedence' && options[key].length) {
this.metadataPrecedence = [...options[key]]
} else if (this[key] !== undefined) {
this[key] = options[key]
}
}
}
toJSON() {
return {
forceRescan: this.forceRescan,
metadataPrecedence: this.metadataPrecedence,
parseSubtitles: this.parseSubtitles,
findCovers: this.findCovers,
coverDestination: this.coverDestination
}
}
setData(options, serverSettings) {
this.forceRescan = !!options.forceRescan
this.parseSubtitles = !!serverSettings.scannerParseSubtitle
this.findCovers = !!serverSettings.scannerFindCovers
this.coverDestination = serverSettings.coverDestination
}
}
module.exports = ScanOptions

172
server/scanner/Scanner.js Normal file
View File

@ -0,0 +1,172 @@
const fs = require('fs-extra')
const Path = require('path')
// Utils
const Logger = require('../Logger')
const { version } = require('../../package.json')
const audioFileScanner = require('../utils/audioFileScanner')
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
const { comparePaths, getIno, getId } = require('../utils/index')
const { secondsToTimestamp } = require('../utils/fileUtils')
const { ScanResult, CoverDestination } = require('../utils/constants')
const AudioFileScanner = require('./AudioFileScanner')
const BookFinder = require('../BookFinder')
const Audiobook = require('../objects/Audiobook')
const LibraryScan = require('./LibraryScan')
const ScanOptions = require('./ScanOptions')
class Scanner {
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) {
this.AudiobookPath = AUDIOBOOK_PATH
this.MetadataPath = METADATA_PATH
this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
this.db = db
this.coverController = coverController
this.emitter = emitter
this.cancelScan = false
this.cancelLibraryScan = {}
this.librariesScanning = []
this.bookFinder = new BookFinder()
}
async scan(libraryId, options = {}) {
if (this.librariesScanning.includes(libraryId)) {
Logger.error(`[Scanner] Already scanning ${libraryId}`)
return
}
var library = this.db.libraries.find(lib => lib.id === libraryId)
if (!library) {
Logger.error(`[Scanner] Library not found for scan ${libraryId}`)
return
} else if (!library.folders.length) {
Logger.warn(`[Scanner] Library has no folders to scan "${library.name}"`)
return
}
var scanOptions = new ScanOptions()
scanOptions.setData(options, this.db.serverSettings)
var libraryScan = new LibraryScan()
libraryScan.setData(library, scanOptions)
Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`)
var results = await this.scanLibrary(libraryScan)
Logger.info(`[Scanner] Library scan ${libraryScan.id} complete`)
return results
}
async scanLibrary(libraryScan) {
var audiobookDataFound = []
for (let i = 0; i < libraryScan.folders.length; i++) {
var folder = libraryScan.folders[i]
var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings)
Logger.debug(`[Scanner] ${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`)
audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder)
}
// Remove audiobooks with no inode
audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId)
const audiobooksToUpdate = []
const audiobooksToRescan = []
const newAudiobookData = []
// Check for existing & removed audiobooks
for (let i = 0; i < audiobooksInLibrary.length; i++) {
var audiobook = audiobooksInLibrary[i]
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
if (!dataFound) {
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
audiobook.isMissing = true
audiobook.lastUpdate = Date.now()
scanResults.missing++
audiobooksToUpdate.push(audiobook)
} else {
var checkRes = audiobook.checkShouldRescan(dataFound)
if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length) {
// existing audiobook has new files
checkRes.audiobook = audiobook
audiobooksToRescan.push(checkRes)
} else if (checkRes.updated) {
audiobooksToUpdate.push(audiobook)
}
// Remove this abf
audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino)
}
}
// Potential NEW Audiobooks
for (let i = 0; i < audiobookDataFound.length; i++) {
var dataFound = audiobookDataFound[i]
var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook')
if (!hasEbook && !dataFound.audioFiles.length) {
Logger.info(`[Scanner] Directory found "${audiobookDataFound.path}" has no ebook or audio files`)
} else {
newAudiobookData.push(dataFound)
}
}
var rescans = []
for (let i = 0; i < audiobooksToRescan.length; i++) {
var rescan = this.rescanAudiobook(audiobooksToRescan[i])
rescans.push(rescan)
}
var newscans = []
for (let i = 0; i < newAudiobookData.length; i++) {
var newscan = this.scanNewAudiobook(newAudiobookData[i])
newscans.push(newscan)
}
var rescanResults = await Promise.all(rescans)
var newscanResults = await Promise.all(newscans)
// TODO: Return report
return {
updates: 0,
additions: 0
}
}
// Return scan result payload
async rescanAudiobook(audiobookCheckData) {
const { newAudioFileData, newOtherFileData, audiobook } = audiobookCheckData
if (newAudioFileData.length) {
var newAudioFiles = await this.scanAudioFiles(newAudioFileData)
// TODO: Update audiobook tracks
}
if (newOtherFileData.length) {
// TODO: Check other files
}
return {
updated: true
}
}
async scanNewAudiobook(audiobookData) {
// TODO: Return new audiobook
return null
}
async scanAudioFiles(audioFileData) {
var proms = []
for (let i = 0; i < audioFileData.length; i++) {
var prom = AudioFileScanner.scan(audioFileData[i])
proms.push(prom)
}
return Promise.all(proms)
}
}
module.exports = Scanner

View File

@ -13,7 +13,7 @@ function getDefaultAudioStream(audioStreams) {
async function scan(path, verbose = false) { async function scan(path, verbose = false) {
Logger.debug(`Scanning path "${path}"`) Logger.debug(`Scanning path "${path}"`)
var probeData = await prober(path, verbose) var probeData = await prober.probe(path, verbose)
if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) { if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
return { return {
error: 'Invalid audio file' error: 'Invalid audio file'

View File

@ -89,10 +89,17 @@ function setFileOwner(path, uid, gid) {
} }
module.exports.setFileOwner = setFileOwner module.exports.setFileOwner = setFileOwner
async function recurseFiles(path) { async function recurseFiles(path, relPathToReplace = null) {
path = path.replace(/\\/g, '/') path = path.replace(/\\/g, '/')
if (!path.endsWith('/')) path = path + '/' if (!path.endsWith('/')) path = path + '/'
if (relPathToReplace) {
relPathToReplace = relPathToReplace.replace(/\\/g, '/')
if (!relPathToReplace.endsWith('/')) relPathToReplace += '/'
} else {
relPathToReplace = path
}
const options = { const options = {
mode: rra.LIST, mode: rra.LIST,
recursive: true, recursive: true,
@ -116,7 +123,7 @@ async function recurseFiles(path) {
} }
// Ignore any file if a directory or the filename starts with "." // Ignore any file if a directory or the filename starts with "."
var relpath = item.fullname.replace(path, '') var relpath = item.fullname.replace(relPathToReplace, '')
var pathStartsWithPeriod = relpath.split('/').find(p => p.startsWith('.')) var pathStartsWithPeriod = relpath.split('/').find(p => p.startsWith('.'))
if (pathStartsWithPeriod) { if (pathStartsWithPeriod) {
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`) Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
@ -126,9 +133,9 @@ async function recurseFiles(path) {
return true return true
}).map((item) => ({ }).map((item) => ({
name: item.name, name: item.name,
path: item.fullname.replace(path, ''), path: item.fullname.replace(relPathToReplace, ''),
dirpath: item.path, dirpath: item.path,
reldirpath: item.path.replace(path, ''), reldirpath: item.path.replace(relPathToReplace, ''),
fullpath: item.fullname, fullpath: item.fullname,
extension: item.extension, extension: item.extension,
deep: item.deep deep: item.deep

View File

@ -1,5 +1,8 @@
var Ffmpeg = require('fluent-ffmpeg') var Ffmpeg = require('fluent-ffmpeg')
const Path = require('path') const Path = require('path')
const AudioProbeData = require('../scanner/AudioProbeData')
const Logger = require('../Logger') const Logger = require('../Logger')
function tryGrabBitRate(stream, all_streams, total_bit_rate) { function tryGrabBitRate(stream, all_streams, total_bit_rate) {
@ -241,4 +244,31 @@ function probe(filepath, verbose = false) {
}) })
}) })
} }
module.exports = probe module.exports.probe = probe
// Updated probe returns AudioProbeData object
function probe2(filepath, verbose = false) {
return new Promise((resolve) => {
Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => {
if (err) {
console.error(err)
var errorMsg = err ? err.message : null
resolve({
error: errorMsg || 'Probe Error'
})
} else {
var rawProbeData = parseProbeData(raw, verbose)
if (!rawProbeData || !rawProbeData.audio_streams.length) {
resolve({
error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed'
})
} else {
var probeData = new AudioProbeData()
probeData.setData(rawProbeData)
resolve(probeData)
}
}
})
})
}
module.exports.probe2 = probe2

View File

@ -267,7 +267,7 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
async function getAudiobookFileData(folder, audiobookPath, serverSettings = {}) { async function getAudiobookFileData(folder, audiobookPath, serverSettings = {}) {
var parseSubtitle = !!serverSettings.scannerParseSubtitle var parseSubtitle = !!serverSettings.scannerParseSubtitle
var fileItems = await recurseFiles(audiobookPath) var fileItems = await recurseFiles(audiobookPath, folder.fullPath)
audiobookPath = audiobookPath.replace(/\\/g, '/') audiobookPath = audiobookPath.replace(/\\/g, '/')
var folderFullPath = folder.fullPath.replace(/\\/g, '/') var folderFullPath = folder.fullPath.replace(/\\/g, '/')