mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-02 20:19:10 +01:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
c0a4ec23d7
@ -158,7 +158,7 @@ export default {
|
||||
}
|
||||
})
|
||||
this.$axios
|
||||
.patch(`/api/user/audiobooks`, updateProgressPayloads)
|
||||
.patch(`/api/me/audiobook/batch/update`, updateProgressPayloads)
|
||||
.then(() => {
|
||||
this.$toast.success('Batch update success!')
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
@ -177,7 +177,7 @@ export default {
|
||||
this.processingBatchDelete = true
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
this.$axios
|
||||
.$post(`/api/audiobooks/delete`, {
|
||||
.$post(`/api/books/batch/delete`, {
|
||||
audiobookIds: this.selectedAudiobooks
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -162,7 +162,7 @@ export default {
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
|
@ -326,7 +326,7 @@ export default {
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
|
@ -131,7 +131,7 @@ export default {
|
||||
}
|
||||
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)
|
||||
return []
|
||||
})
|
||||
|
@ -171,7 +171,7 @@ export default {
|
||||
this.processing = true
|
||||
console.log('Calling update', account)
|
||||
this.$axios
|
||||
.$patch(`/api/user/${this.account.id}`, account)
|
||||
.$patch(`/api/users/${this.account.id}`, account)
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
@ -198,7 +198,7 @@ export default {
|
||||
var account = { ...this.newUser }
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post('/api/user', account)
|
||||
.$post('/api/users', account)
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
|
@ -94,7 +94,7 @@ export default {
|
||||
this.processing = true
|
||||
var collectionName = this.collectionName
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${this.collection.id}`)
|
||||
.$delete(`/api/collections/${this.collection.id}`)
|
||||
.then(() => {
|
||||
this.processing = false
|
||||
this.show = false
|
||||
@ -122,7 +122,7 @@ export default {
|
||||
description: this.newCollectionDescription || null
|
||||
}
|
||||
this.$axios
|
||||
.$patch(`/api/collection/${this.collection.id}`, collectionUpdate)
|
||||
.$patch(`/api/collections/${this.collection.id}`, collectionUpdate)
|
||||
.then((collection) => {
|
||||
console.log('Collection Updated', collection)
|
||||
this.processing = false
|
||||
|
@ -220,7 +220,7 @@ export default {
|
||||
async fetchFull() {
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
|
||||
|
@ -96,7 +96,7 @@ export default {
|
||||
this.processing = true
|
||||
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${collection.id}/book/${this.selectedAudiobookId}`)
|
||||
.$delete(`/api/collections/${collection.id}/book/${this.selectedAudiobookId}`)
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book removed from collection`, updatedCollection)
|
||||
this.$toast.success('Book removed from collection')
|
||||
@ -114,7 +114,7 @@ export default {
|
||||
this.processing = true
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/collection/${collection.id}/book`, { id: this.selectedAudiobookId })
|
||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedAudiobookId })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book added to collection`, updatedCollection)
|
||||
this.$toast.success('Book added to collection')
|
||||
|
@ -154,7 +154,7 @@ export default {
|
||||
var coverPayload = {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -171,7 +171,7 @@ export default {
|
||||
var bookUpdatePayload = {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
|
@ -155,7 +155,7 @@ export default {
|
||||
form.set('cover', this.selectedFile)
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/audiobook/${this.audiobook.id}/cover`, form)
|
||||
.$post(`/api/books/${this.audiobook.id}/cover`, form)
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
@ -217,7 +217,7 @@ export default {
|
||||
|
||||
// Download cover from url and use
|
||||
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)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
@ -231,7 +231,7 @@ export default {
|
||||
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)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
@ -266,7 +266,7 @@ export default {
|
||||
setCover(coverFile) {
|
||||
this.isProcessing = true
|
||||
this.$axios
|
||||
.$patch(`/api/audiobook/${this.audiobook.id}/coverfile`, coverFile)
|
||||
.$patch(`/api/books/${this.audiobook.id}/coverfile`, coverFile)
|
||||
.then((data) => {
|
||||
console.log('response data', data)
|
||||
if (data && typeof data === 'string') {
|
||||
|
@ -195,7 +195,7 @@ export default {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -220,27 +220,11 @@ export default {
|
||||
|
||||
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() {
|
||||
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.$axios
|
||||
.$delete(`/api/audiobook/${this.audiobookId}`)
|
||||
.$delete(`/api/books/${this.audiobookId}`)
|
||||
.then(() => {
|
||||
console.log('Audiobook removed')
|
||||
this.$toast.success('Audiobook Removed')
|
||||
|
@ -133,7 +133,7 @@ export default {
|
||||
publisher: true,
|
||||
publishYear: true,
|
||||
series: true,
|
||||
volumeNumber: true,
|
||||
volumeNumber: true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -198,7 +198,7 @@ export default {
|
||||
publisher: true,
|
||||
publishYear: true,
|
||||
series: true,
|
||||
volumeNumber: true,
|
||||
volumeNumber: true
|
||||
}
|
||||
|
||||
if (this.audiobook.id !== this.audiobookId) {
|
||||
@ -238,7 +238,7 @@ export default {
|
||||
var coverPayload = {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -255,7 +255,7 @@ export default {
|
||||
var bookUpdatePayload = {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
|
@ -105,7 +105,7 @@ export default {
|
||||
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$patch(`/api/library/${this.library.id}`, newLibraryPayload)
|
||||
.$patch(`/api/libraries/${this.library.id}`, newLibraryPayload)
|
||||
.then((res) => {
|
||||
this.$emit('update:processing', false)
|
||||
this.$emit('close')
|
||||
@ -137,7 +137,7 @@ export default {
|
||||
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$post('/api/library', newLibraryPayload)
|
||||
.$post('/api/libraries', newLibraryPayload)
|
||||
.then((res) => {
|
||||
this.$emit('update:processing', false)
|
||||
this.$emit('close')
|
||||
|
@ -72,7 +72,7 @@ export default {
|
||||
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
|
||||
this.isDeleting = true
|
||||
this.$axios
|
||||
.$delete(`/api/library/${this.library.id}`)
|
||||
.$delete(`/api/libraries/${this.library.id}`)
|
||||
.then((data) => {
|
||||
this.isDeleting = false
|
||||
if (data.error) {
|
||||
|
@ -68,7 +68,7 @@ export default {
|
||||
books: this.booksCopy.map((b) => b.id)
|
||||
}
|
||||
this.$axios
|
||||
.$patch(`/api/collection/${this.collectionId}`, collectionUpdate)
|
||||
.$patch(`/api/collections/${this.collectionId}`, collectionUpdate)
|
||||
.then((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}"?`)) {
|
||||
this.isDeletingUser = true
|
||||
this.$axios
|
||||
.$delete(`/api/user/${user.id}`)
|
||||
.$delete(`/api/users/${user.id}`)
|
||||
.then((data) => {
|
||||
this.isDeletingUser = false
|
||||
if (data.error) {
|
||||
|
@ -140,7 +140,7 @@ export default {
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.book.id}`, updatePayload)
|
||||
.$patch(`/api/me/audiobook/${this.book.id}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(`"${this.bookTitle}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
@ -155,7 +155,7 @@ export default {
|
||||
this.processingRemove = true
|
||||
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${this.collectionId}/book/${this.book.id}`)
|
||||
.$delete(`/api/collections/${this.collectionId}/book/${this.book.id}`)
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book removed from collection`, updatedCollection)
|
||||
this.$toast.success('Book removed from collection')
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.6.23",
|
||||
"version": "1.6.26",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -90,7 +90,7 @@ export default {
|
||||
}
|
||||
this.changingPassword = true
|
||||
this.$axios
|
||||
.$patch('/api/user/password', {
|
||||
.$patch('/api/me/password', {
|
||||
password: this.password,
|
||||
newPassword: this.newPassword
|
||||
})
|
||||
|
@ -115,7 +115,7 @@ export default {
|
||||
if (!store.getters['user/getUserCanUpdate']) {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -291,7 +291,7 @@ export default {
|
||||
|
||||
this.saving = true
|
||||
this.$axios
|
||||
.$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||
.$patch(`/api/books/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||
.then((data) => {
|
||||
console.log('Finished patching files', data)
|
||||
this.saving = false
|
||||
|
@ -161,7 +161,7 @@ export default {
|
||||
if (!store.state.user.user) {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -383,7 +383,7 @@ export default {
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
@ -417,7 +417,7 @@ export default {
|
||||
audiobookUpdated() {
|
||||
console.log('Audiobook Updated - Fetch full audiobook')
|
||||
this.$axios
|
||||
.$get(`/api/audiobook/${this.audiobookId}`)
|
||||
.$get(`/api/books/${this.audiobookId}`)
|
||||
.then((audiobook) => {
|
||||
console.log('Updated audiobook', audiobook)
|
||||
this.audiobook = audiobook
|
||||
@ -430,7 +430,7 @@ export default {
|
||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||
this.resettingProgress = true
|
||||
this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`)
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}/reset-progress`)
|
||||
.then(() => {
|
||||
console.log('Progress reset complete')
|
||||
this.$toast.success(`Your progress was reset`)
|
||||
|
@ -169,7 +169,7 @@ export default {
|
||||
this.isProcessing = true
|
||||
|
||||
this.$axios
|
||||
.$post('/api/audiobooks/update', this.audiobookCopies)
|
||||
.$post('/api/books/batch/update', this.audiobookCopies)
|
||||
.then((data) => {
|
||||
this.isProcessing = false
|
||||
if (data.updates) {
|
||||
|
@ -44,7 +44,7 @@ export default {
|
||||
if (!store.state.user.user) {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -105,7 +105,7 @@ export default {
|
||||
this.processingRemove = true
|
||||
var collectionName = this.collectionName
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${this.collection.id}`)
|
||||
.$delete(`/api/collections/${this.collection.id}`)
|
||||
.then(() => {
|
||||
this.processingRemove = false
|
||||
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?')) {
|
||||
this.isResettingAudiobooks = true
|
||||
this.$axios
|
||||
.$delete('/api/audiobooks')
|
||||
.$delete('/api/books/all')
|
||||
.then(() => {
|
||||
this.isResettingAudiobooks = false
|
||||
this.$toast.success('Successfully reset audiobooks')
|
||||
|
@ -97,7 +97,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
return []
|
||||
})
|
||||
|
@ -71,7 +71,7 @@
|
||||
<script>
|
||||
export default {
|
||||
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)
|
||||
return null
|
||||
})
|
||||
@ -115,11 +115,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
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)
|
||||
return []
|
||||
})
|
||||
|
@ -31,7 +31,7 @@ export default {
|
||||
if (params.id === 'search' && 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)
|
||||
return {}
|
||||
})
|
||||
@ -92,7 +92,7 @@ export default {
|
||||
methods: {
|
||||
async newQuery() {
|
||||
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)
|
||||
return {}
|
||||
})
|
||||
|
@ -211,7 +211,7 @@ export const actions = {
|
||||
commit('setLoadedLibrary', currentLibraryId)
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/library/${currentLibraryId}/audiobooks`)
|
||||
.$get(`/api/libraries/${currentLibraryId}/books`)
|
||||
.then((data) => {
|
||||
commit('set', data)
|
||||
commit('setLastLoad')
|
||||
|
@ -60,7 +60,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
return this.$axios
|
||||
.$get(`/api/library/${libraryId}`)
|
||||
.$get(`/api/libraries/${libraryId}`)
|
||||
.then((data) => {
|
||||
commit('addUpdate', data)
|
||||
commit('setCurrentLibrary', libraryId)
|
||||
|
@ -64,7 +64,7 @@ export const actions = {
|
||||
}
|
||||
// Immediately update
|
||||
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) {
|
||||
commit('setSettings', result.settings)
|
||||
return true
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.6.23",
|
||||
"version": "1.6.26",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@ -50,4 +50,4 @@
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -355,9 +355,6 @@ class Scanner {
|
||||
Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
|
||||
}
|
||||
|
||||
// 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
|
||||
// Note: inode will not change on rename
|
||||
var hasUpdatedIno = false
|
||||
@ -457,7 +454,6 @@ class Scanner {
|
||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||
|
||||
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
|
||||
// TEMP - update ino for each audiobook
|
||||
if (audiobooksInLibrary.length) {
|
||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||
var ab = audiobooksInLibrary[i]
|
||||
@ -466,7 +462,7 @@ class Scanner {
|
||||
if (shouldUpdateIno) {
|
||||
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)
|
||||
|
||||
var hasUpdates = await ab.checkUpdateInos()
|
||||
@ -507,7 +503,7 @@ class Scanner {
|
||||
// Check for removed audiobooks
|
||||
for (let i = 0; i < audiobooksInLibrary.length; 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) {
|
||||
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (this.chapters.length !== updatedChapters.length) {
|
||||
this.chapters = updatedChapters.map(ch => ({ ...ch }))
|
||||
|
@ -353,6 +353,7 @@ class Audiobook {
|
||||
if (imageFile) {
|
||||
data.coverFullPath = imageFile.fullPath
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -822,5 +823,141 @@ class Audiobook {
|
||||
var audioFile = this.audioFiles[0]
|
||||
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
|
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) {
|
||||
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) {
|
||||
return {
|
||||
error: 'Invalid audio file'
|
||||
|
@ -89,10 +89,17 @@ function setFileOwner(path, uid, gid) {
|
||||
}
|
||||
module.exports.setFileOwner = setFileOwner
|
||||
|
||||
async function recurseFiles(path) {
|
||||
async function recurseFiles(path, relPathToReplace = null) {
|
||||
path = path.replace(/\\/g, '/')
|
||||
if (!path.endsWith('/')) path = path + '/'
|
||||
|
||||
if (relPathToReplace) {
|
||||
relPathToReplace = relPathToReplace.replace(/\\/g, '/')
|
||||
if (!relPathToReplace.endsWith('/')) relPathToReplace += '/'
|
||||
} else {
|
||||
relPathToReplace = path
|
||||
}
|
||||
|
||||
const options = {
|
||||
mode: rra.LIST,
|
||||
recursive: true,
|
||||
@ -116,7 +123,7 @@ async function recurseFiles(path) {
|
||||
}
|
||||
|
||||
// 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('.'))
|
||||
if (pathStartsWithPeriod) {
|
||||
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
|
||||
@ -126,9 +133,9 @@ async function recurseFiles(path) {
|
||||
return true
|
||||
}).map((item) => ({
|
||||
name: item.name,
|
||||
path: item.fullname.replace(path, ''),
|
||||
path: item.fullname.replace(relPathToReplace, ''),
|
||||
dirpath: item.path,
|
||||
reldirpath: item.path.replace(path, ''),
|
||||
reldirpath: item.path.replace(relPathToReplace, ''),
|
||||
fullpath: item.fullname,
|
||||
extension: item.extension,
|
||||
deep: item.deep
|
||||
|
@ -1,5 +1,8 @@
|
||||
var Ffmpeg = require('fluent-ffmpeg')
|
||||
const Path = require('path')
|
||||
|
||||
const AudioProbeData = require('../scanner/AudioProbeData')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
|
||||
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 = {}) {
|
||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||
|
||||
var fileItems = await recurseFiles(audiobookPath)
|
||||
var fileItems = await recurseFiles(audiobookPath, folder.fullPath)
|
||||
|
||||
audiobookPath = audiobookPath.replace(/\\/g, '/')
|
||||
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
||||
|
Loading…
Reference in New Issue
Block a user