mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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": { | ||||||
|  | |||||||
										
											
												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