mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-02 20:19:10 +01:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
c0a4ec23d7
@ -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(() => {
|
||||||
|
@ -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'}`)
|
||||||
|
@ -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'}`)
|
||||||
|
@ -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 []
|
||||||
})
|
})
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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') {
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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')
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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) {
|
||||||
|
@ -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')
|
||||||
|
@ -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": {
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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`)
|
||||||
|
@ -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) {
|
||||||
|
@ -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`)
|
||||||
|
@ -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')
|
||||||
|
@ -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 []
|
||||||
})
|
})
|
||||||
|
@ -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 []
|
||||||
})
|
})
|
||||||
|
@ -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 {}
|
||||||
})
|
})
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
@ -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
|
||||||
|
31
server/controllers/BackupController.js
Normal file
31
server/controllers/BackupController.js
Normal 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()
|
220
server/controllers/BookController.js
Normal file
220
server/controllers/BookController.js
Normal 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()
|
97
server/controllers/CollectionController.js
Normal file
97
server/controllers/CollectionController.js
Normal 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()
|
205
server/controllers/LibraryController.js
Normal file
205
server/controllers/LibraryController.js
Normal 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()
|
96
server/controllers/MeController.js
Normal file
96
server/controllers/MeController.js
Normal 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()
|
152
server/controllers/UserController.js
Normal file
152
server/controllers/UserController.js
Normal 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()
|
@ -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 }))
|
||||||
|
@ -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
|
22
server/scanner/AudioFileScanner.js
Normal file
22
server/scanner/AudioFileScanner.js
Normal 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()
|
74
server/scanner/AudioProbeData.js
Normal file
74
server/scanner/AudioProbeData.js
Normal 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
|
34
server/scanner/LibraryScan.js
Normal file
34
server/scanner/LibraryScan.js
Normal 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
|
68
server/scanner/ScanOptions.js
Normal file
68
server/scanner/ScanOptions.js
Normal 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
172
server/scanner/Scanner.js
Normal 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
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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, '/')
|
||||||
|
Loading…
Reference in New Issue
Block a user