mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-11 08:18:17 +02:00
New data model save covers, scanner, new api routes
This commit is contained in:
parent
5f4e5cd3d8
commit
73257188f6
@ -425,42 +425,42 @@ export default {
|
|||||||
this.handleScroll(scrollTop)
|
this.handleScroll(scrollTop)
|
||||||
// }, 250)
|
// }, 250)
|
||||||
},
|
},
|
||||||
audiobookAdded(audiobook) {
|
libraryItemAdded(libraryItem) {
|
||||||
console.log('Audiobook added', audiobook)
|
console.log('libraryItem added', libraryItem)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
// TODO: Check if audiobook would be on this shelf
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
},
|
},
|
||||||
audiobookUpdated(audiobook) {
|
libraryItemUpdated(libraryItem) {
|
||||||
console.log('Audiobook updated', audiobook)
|
console.log('Item updated', libraryItem)
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities[indexOf] = audiobook
|
this.entities[indexOf] = libraryItem
|
||||||
if (this.entityComponentRefs[indexOf]) {
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
this.entityComponentRefs[indexOf].setEntity(audiobook)
|
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobookRemoved(audiobook) {
|
libraryItemRemoved(libraryItem) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities = this.entities.filter((ent) => ent.id !== audiobook.id)
|
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
||||||
this.totalEntities = this.entities.length
|
this.totalEntities = this.entities.length
|
||||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||||
this.remountEntities()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobooksAdded(audiobooks) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('audiobooks added', audiobooks)
|
console.log('items added', libraryItems)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
// TODO: Check if audiobook would be on this shelf
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
},
|
},
|
||||||
audiobooksUpdated(audiobooks) {
|
libraryItemsUpdated(libraryItems) {
|
||||||
audiobooks.forEach((ab) => {
|
libraryItems.forEach((ab) => {
|
||||||
this.audiobookUpdated(ab)
|
this.libraryItemUpdated(ab)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initSizeData(_bookshelf) {
|
initSizeData(_bookshelf) {
|
||||||
@ -525,11 +525,11 @@ export default {
|
|||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.on('audiobook_updated', this.audiobookUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
this.$root.socket.on('audiobook_added', this.audiobookAdded)
|
this.$root.socket.on('item_added', this.libraryItemAdded)
|
||||||
this.$root.socket.on('audiobook_removed', this.audiobookRemoved)
|
this.$root.socket.on('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.on('audiobooks_added', this.audiobooksAdded)
|
this.$root.socket.on('items_added', this.libraryItemsAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
@ -546,11 +546,11 @@ export default {
|
|||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('audiobook_updated', this.audiobookUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
this.$root.socket.off('audiobook_added', this.audiobookAdded)
|
this.$root.socket.off('item_added', this.libraryItemAdded)
|
||||||
this.$root.socket.off('audiobook_removed', this.audiobookRemoved)
|
this.$root.socket.off('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.off('audiobooks_added', this.audiobooksAdded)
|
this.$root.socket.off('items_added', this.libraryItemsAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
|
@ -379,8 +379,8 @@ export default {
|
|||||||
this.isSelectionMode = val
|
this.isSelectionMode = val
|
||||||
if (!val) this.selected = false
|
if (!val) this.selected = false
|
||||||
},
|
},
|
||||||
setEntity(audiobook) {
|
setEntity(libraryItem) {
|
||||||
this.audiobook = audiobook
|
this.audiobook = libraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
|
@ -157,7 +157,7 @@ export default {
|
|||||||
.filter((f) => f.fileType === 'image')
|
.filter((f) => f.fileType === 'image')
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
var _file = { ...file }
|
var _file = { ...file }
|
||||||
_file.localPath = `/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath)}`
|
_file.localPath = `/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}`
|
||||||
return _file
|
return _file
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ export default {
|
|||||||
form.set('cover', this.selectedFile)
|
form.set('cover', this.selectedFile)
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/books/${this.libraryItemId}/cover`, form)
|
.$post(`/api/items/${this.libraryItemId}/cover`, form)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
@ -230,8 +230,20 @@ export default {
|
|||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
var success = false
|
var success = false
|
||||||
|
|
||||||
// Download cover from url and use
|
if (!cover) {
|
||||||
if (cover.startsWith('http:') || cover.startsWith('https:')) {
|
// Remove cover
|
||||||
|
success = await this.$axios
|
||||||
|
.$delete(`/api/items/${this.libraryItemId}/cover`)
|
||||||
|
.then(() => true)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove cover', error)
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
this.$toast.error(error.response.data)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else if (cover.startsWith('http:') || cover.startsWith('https:')) {
|
||||||
|
// Download cover from url and use
|
||||||
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => {
|
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/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) {
|
||||||
@ -242,11 +254,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
// Update local cover url
|
// Update local cover url
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
book: {
|
cover
|
||||||
cover: cover
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, updatePayload).catch((error) => {
|
success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, 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)
|
||||||
@ -256,7 +266,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
this.$toast.success('Update Successful')
|
this.$toast.success('Update Successful')
|
||||||
this.$emit('close')
|
// this.$emit('close')
|
||||||
} else {
|
} else {
|
||||||
this.imageUrl = this.media.coverPath || ''
|
this.imageUrl = this.media.coverPath || ''
|
||||||
}
|
}
|
||||||
|
@ -287,22 +287,22 @@ export default {
|
|||||||
this.quickMatching = false
|
this.quickMatching = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
audiobookScanComplete(result) {
|
libraryScanComplete(result) {
|
||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||||
} else if (result === 'UPDATED') {
|
} else if (result === 'UPDATED') {
|
||||||
this.$toast.success(`Re-Scan complete audiobook was updated`)
|
this.$toast.success(`Re-Scan complete item was updated`)
|
||||||
} else if (result === 'UPTODATE') {
|
} else if (result === 'UPTODATE') {
|
||||||
this.$toast.success(`Re-Scan complete audiobook was up to date`)
|
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||||
} else if (result === 'REMOVED') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete audiobook was removed`)
|
this.$toast.error(`Re-Scan complete item was removed`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
this.$root.socket.once('item_scan_complete', this.libraryScanComplete)
|
||||||
this.$root.socket.emit('scan_audiobook', this.audiobookId)
|
this.$root.socket.emit('scan_item', this.libraryItemId)
|
||||||
},
|
},
|
||||||
saveMetadataComplete(result) {
|
saveMetadataComplete(result) {
|
||||||
this.savingMetadata = false
|
this.savingMetadata = false
|
||||||
@ -381,7 +381,7 @@ export default {
|
|||||||
if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) {
|
if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/books/${this.libraryItemId}`)
|
.$delete(`/api/items/${this.libraryItemId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Item removed')
|
console.log('Item removed')
|
||||||
this.$toast.success('Item Removed')
|
this.$toast.success('Item Removed')
|
||||||
|
@ -13,20 +13,20 @@
|
|||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable break-all">
|
||||||
<tr class="font-book">
|
<tr class="font-book">
|
||||||
<th>#</th>
|
<th class="w-16">#</th>
|
||||||
<th class="text-left">Filename</th>
|
<th class="text-left">Filename</th>
|
||||||
<th class="text-left">Size</th>
|
<th class="text-left w-24 min-w-24">Size</th>
|
||||||
<th class="text-left">Duration</th>
|
<th class="text-left w-24 min-w-24">Duration</th>
|
||||||
<th v-if="showDownload" class="text-center">Download</th>
|
<th v-if="showDownload" class="text-center w-24 min-w-24">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracks">
|
<template v-for="track in tracks">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p>{{ track.index }}</p>
|
<p>{{ track.index }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-sans">{{ showFullPath ? track.path : track.filename }}</td>
|
<td class="font-sans">{{ showFullPath ? track.metadata.path : track.metadata.filename }}</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $bytesPretty(track.metadata.size) }}
|
{{ $bytesPretty(track.metadata.size) }}
|
||||||
</td>
|
</td>
|
||||||
|
@ -16,18 +16,22 @@
|
|||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr class="font-book">
|
||||||
<th class="text-left px-4">Path</th>
|
<th class="text-left px-4">Path</th>
|
||||||
|
<th class="text-left w-24 min-w-24">Size</th>
|
||||||
<th class="text-left px-4 w-24">Filetype</th>
|
<th class="text-left px-4 w-24">Filetype</th>
|
||||||
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in files">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book pl-2">
|
<td class="font-book px-4">
|
||||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="font-mono">
|
||||||
|
{{ $bytesPretty(file.metadata.size) }}
|
||||||
|
</td>
|
||||||
<td class="text-xs">
|
<td class="text-xs">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
|
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
|
||||||
<p>{{ file.metadata.ext }}</p>
|
<p>{{ file.fileType }}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload && !isMissing" class="text-center">
|
<td v-if="userCanDownload && !isMissing" class="text-center">
|
||||||
|
@ -171,24 +171,6 @@ export default {
|
|||||||
// this.$store.commit('audiobooks/addUpdate', audiobook)
|
// this.$store.commit('audiobooks/addUpdate', audiobook)
|
||||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||||
},
|
},
|
||||||
audiobookUpdated(audiobook) {
|
|
||||||
if (this.$store.state.selectedAudiobook && this.$store.state.selectedAudiobook.id === audiobook.id) {
|
|
||||||
console.log('Updating selected audiobook', audiobook)
|
|
||||||
this.$store.commit('setSelectedAudiobook', audiobook)
|
|
||||||
}
|
|
||||||
// Just triggers the listeners
|
|
||||||
this.$store.commit('audiobooks/audiobookUpdated', audiobook)
|
|
||||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
|
||||||
// this.$store.commit('audiobooks/addUpdate', audiobook)
|
|
||||||
},
|
|
||||||
audiobookRemoved(audiobook) {
|
|
||||||
if (this.$route.name.startsWith('audiobook')) {
|
|
||||||
if (this.$route.params.id === audiobook.id) {
|
|
||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// this.$store.commit('audiobooks/remove', audiobook)
|
|
||||||
},
|
|
||||||
audiobooksAdded(audiobooks) {
|
audiobooksAdded(audiobooks) {
|
||||||
audiobooks.forEach((ab) => {
|
audiobooks.forEach((ab) => {
|
||||||
this.audiobookAdded(ab)
|
this.audiobookAdded(ab)
|
||||||
@ -215,6 +197,13 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
||||||
},
|
},
|
||||||
|
libraryItemRemoved(item) {
|
||||||
|
if (this.$route.name.startsWith('item')) {
|
||||||
|
if (this.$route.params.id === item.id) {
|
||||||
|
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
scanComplete(data) {
|
scanComplete(data) {
|
||||||
console.log('Scan complete received', data)
|
console.log('Scan complete received', data)
|
||||||
|
|
||||||
@ -403,6 +392,7 @@ export default {
|
|||||||
|
|
||||||
// Library Item Listeners
|
// Library Item Listeners
|
||||||
this.socket.on('item_updated', this.libraryItemUpdated)
|
this.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
|
this.socket.on('item_removed', this.libraryItemRemoved)
|
||||||
|
|
||||||
// User Listeners
|
// User Listeners
|
||||||
this.socket.on('user_updated', this.userUpdated)
|
this.socket.on('user_updated', this.userUpdated)
|
||||||
|
@ -76,8 +76,12 @@ class ApiController {
|
|||||||
//
|
//
|
||||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this))
|
this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this))
|
||||||
|
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this))
|
||||||
this.router.patch('/items/:id/media', LibraryItemController.middleware.bind(this), LibraryItemController.updateMedia.bind(this))
|
this.router.patch('/items/:id/media', LibraryItemController.middleware.bind(this), LibraryItemController.updateMedia.bind(this))
|
||||||
this.router.get('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.getCover.bind(this))
|
this.router.get('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.getCover.bind(this))
|
||||||
|
this.router.post('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.uploadCover.bind(this))
|
||||||
|
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
|
||||||
|
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Book Routes
|
// Book Routes
|
||||||
@ -437,18 +441,18 @@ class ApiController {
|
|||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteAudiobook(audiobook) {
|
async handleDeleteLibraryItem(libraryItem) {
|
||||||
// Remove audiobook from users
|
// Remove libraryItem from users
|
||||||
for (let i = 0; i < this.db.users.length; i++) {
|
for (let i = 0; i < this.db.users.length; i++) {
|
||||||
var user = this.db.users[i]
|
var user = this.db.users[i]
|
||||||
var madeUpdates = user.deleteAudiobookData(audiobook.id)
|
var madeUpdates = user.deleteAudiobookData(libraryItem.id)
|
||||||
if (madeUpdates) {
|
if (madeUpdates) {
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any streams open for this audiobook
|
// remove any streams open for this audiobook
|
||||||
var streams = this.streamManager.streams.filter(stream => stream.audiobookId === audiobook.id)
|
var streams = this.streamManager.streams.filter(stream => stream.audiobookId === libraryItem.id)
|
||||||
for (let i = 0; i < streams.length; i++) {
|
for (let i = 0; i < streams.length; i++) {
|
||||||
var stream = streams[i]
|
var stream = streams[i]
|
||||||
var client = stream.client
|
var client = stream.client
|
||||||
@ -461,22 +465,22 @@ class ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove book from collections
|
// remove book from collections
|
||||||
var collectionsWithBook = this.db.collections.filter(c => c.books.includes(audiobook.id))
|
var collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
|
||||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
for (let i = 0; i < collectionsWithBook.length; i++) {
|
||||||
var collection = collectionsWithBook[i]
|
var collection = collectionsWithBook[i]
|
||||||
collection.removeBook(audiobook.id)
|
collection.removeBook(libraryItem.id)
|
||||||
await this.db.updateEntity('collection', collection)
|
await this.db.updateEntity('collection', collection)
|
||||||
this.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.audiobooks))
|
this.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// purge cover cache
|
// purge cover cache
|
||||||
if (audiobook.cover) {
|
if (libraryItem.media.coverPath) {
|
||||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var audiobookJSON = audiobook.toJSONMinified()
|
var json = libraryItem.toJSONExpanded()
|
||||||
await this.db.removeEntity('audiobook', audiobook.id)
|
await this.db.removeLibraryItem(libraryItem.id)
|
||||||
this.emitter('audiobook_removed', audiobookJSON)
|
this.emitter('item_removed', json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserListeningSessionsHelper(userId) {
|
async getUserListeningSessionsHelper(userId) {
|
||||||
|
@ -13,12 +13,10 @@ const Logger = require('./Logger')
|
|||||||
const Backup = require('./objects/Backup')
|
const Backup = require('./objects/Backup')
|
||||||
|
|
||||||
class BackupManager {
|
class BackupManager {
|
||||||
constructor(Uid, Gid, db) {
|
constructor(db) {
|
||||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||||
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
||||||
|
|
||||||
this.Uid = Uid
|
|
||||||
this.Gid = Gid
|
|
||||||
this.db = db
|
this.db = db
|
||||||
|
|
||||||
this.scheduleTask = null
|
this.scheduleTask = null
|
||||||
@ -37,7 +35,7 @@ class BackupManager {
|
|||||||
var backupsDirExists = await fs.pathExists(this.BackupPath)
|
var backupsDirExists = await fs.pathExists(this.BackupPath)
|
||||||
if (!backupsDirExists) {
|
if (!backupsDirExists) {
|
||||||
await fs.ensureDir(this.BackupPath)
|
await fs.ensureDir(this.BackupPath)
|
||||||
await filePerms(this.BackupPath, 0o774, this.Uid, this.Gid)
|
await filePerms.setDefault(this.BackupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadBackups()
|
await this.loadBackups()
|
||||||
@ -211,7 +209,7 @@ class BackupManager {
|
|||||||
})
|
})
|
||||||
if (zipResult) {
|
if (zipResult) {
|
||||||
Logger.info(`[BackupManager] Backup successful ${newBackup.id}`)
|
Logger.info(`[BackupManager] Backup successful ${newBackup.id}`)
|
||||||
await filePerms(newBackup.fullPath, 0o774, this.Uid, this.Gid)
|
await filePerms.setDefault(newBackup.fullPath)
|
||||||
newBackup.fileSize = await getFileSize(newBackup.fullPath)
|
newBackup.fileSize = await getFileSize(newBackup.fullPath)
|
||||||
var existingIndex = this.backups.findIndex(b => b.id === newBackup.id)
|
var existingIndex = this.backups.findIndex(b => b.id === newBackup.id)
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
|
@ -42,11 +42,11 @@ class CacheManager {
|
|||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async purgeCoverCache(audiobookId) {
|
async purgeCoverCache(libraryItemId) {
|
||||||
// If purgeAll has been called... The cover cache directory no longer exists
|
// If purgeAll has been called... The cover cache directory no longer exists
|
||||||
await fs.ensureDir(this.CoverCachePath)
|
await fs.ensureDir(this.CoverCachePath)
|
||||||
return Promise.all((await fs.readdir(this.CoverCachePath)).reduce((promises, file) => {
|
return Promise.all((await fs.readdir(this.CoverCachePath)).reduce((promises, file) => {
|
||||||
if (file.startsWith(audiobookId)) {
|
if (file.startsWith(libraryItemId)) {
|
||||||
Logger.debug(`[CacheManager] Going to purge ${file}`);
|
Logger.debug(`[CacheManager] Going to purge ${file}`);
|
||||||
promises.push(this.removeCache(Path.join(this.CoverCachePath, file)))
|
promises.push(this.removeCache(Path.join(this.CoverCachePath, file)))
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ const axios = require('axios')
|
|||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const readChunk = require('read-chunk')
|
const readChunk = require('read-chunk')
|
||||||
const imageType = require('image-type')
|
const imageType = require('image-type')
|
||||||
|
const filePerms = require('./utils/filePerms')
|
||||||
|
|
||||||
const globals = require('./utils/globals')
|
const globals = require('./utils/globals')
|
||||||
const { downloadFile } = require('./utils/fileUtils')
|
const { downloadFile } = require('./utils/fileUtils')
|
||||||
|
const { extractCoverArt } = require('./utils/ffmpegHelpers')
|
||||||
|
|
||||||
class CoverController {
|
class CoverController {
|
||||||
constructor(db, cacheManager) {
|
constructor(db, cacheManager) {
|
||||||
@ -16,17 +18,11 @@ class CoverController {
|
|||||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoverDirectory(audiobook) {
|
getCoverDirectory(libraryItem) {
|
||||||
if (this.db.serverSettings.storeCoverWithBook) {
|
if (this.db.serverSettings.storeCoverWithBook) {
|
||||||
return {
|
return libraryItem.path
|
||||||
fullPath: audiobook.fullPath,
|
|
||||||
relPath: '/s/book/' + audiobook.id
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return Path.posix.join(this.BookMetadataPath, libraryItem.id)
|
||||||
fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id),
|
|
||||||
relPath: Path.posix.join('/metadata', 'books', audiobook.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,18 +63,18 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkFileIsValidImage(imagepath) {
|
async checkFileIsValidImage(imagepath, removeOnInvalid = false) {
|
||||||
const buffer = await readChunk(imagepath, 0, 12)
|
const buffer = await readChunk(imagepath, 0, 12)
|
||||||
const imgType = imageType(buffer)
|
const imgType = imageType(buffer)
|
||||||
if (!imgType) {
|
if (!imgType) {
|
||||||
await this.removeFile(imagepath)
|
if (removeOnInvalid) await this.removeFile(imagepath)
|
||||||
return {
|
return {
|
||||||
error: 'Invalid image'
|
error: 'Invalid image'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!globals.SupportedImageTypes.includes(imgType.ext)) {
|
if (!globals.SupportedImageTypes.includes(imgType.ext)) {
|
||||||
await this.removeFile(imagepath)
|
if (removeOnInvalid) await this.removeFile(imagepath)
|
||||||
return {
|
return {
|
||||||
error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
||||||
}
|
}
|
||||||
@ -86,7 +82,7 @@ class CoverController {
|
|||||||
return imgType
|
return imgType
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadCover(audiobook, coverFile) {
|
async uploadCover(libraryItem, coverFile) {
|
||||||
var extname = Path.extname(coverFile.name.toLowerCase())
|
var extname = Path.extname(coverFile.name.toLowerCase())
|
||||||
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
||||||
return {
|
return {
|
||||||
@ -94,12 +90,10 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var { fullPath, relPath } = this.getCoverDirectory(audiobook)
|
var coverDirPath = this.getCoverDirectory(libraryItem)
|
||||||
await fs.ensureDir(fullPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
var coverFilename = `cover${extname}`
|
var coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
||||||
var coverFullPath = Path.posix.join(fullPath, coverFilename)
|
|
||||||
var coverPath = Path.posix.join(relPath, coverFilename)
|
|
||||||
|
|
||||||
// Move cover from temp upload dir to destination
|
// Move cover from temp upload dir to destination
|
||||||
var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
||||||
@ -113,23 +107,23 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.removeOldCovers(fullPath, extname)
|
await this.removeOldCovers(coverDirPath, extname)
|
||||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
|
Logger.info(`[CoverController] Uploaded libraryItem cover "${coverFullPath}" for "${libraryItem.media.metadata.title}"`)
|
||||||
|
|
||||||
audiobook.updateBookCover(coverPath, coverFullPath)
|
libraryItem.updateMediaCover(coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverPath
|
cover: coverFullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadCoverFromUrl(audiobook, url) {
|
async downloadCoverFromUrl(libraryItem, url) {
|
||||||
try {
|
try {
|
||||||
var { fullPath, relPath } = this.getCoverDirectory(audiobook)
|
var coverDirPath = this.getCoverDirectory(libraryItem)
|
||||||
await fs.ensureDir(fullPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
var temppath = Path.posix.join(fullPath, 'cover')
|
var temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
var success = await downloadFile(url, temppath).then(() => true).catch((err) => {
|
var success = await downloadFile(url, temppath).then(() => true).catch((err) => {
|
||||||
Logger.error(`[CoverController] Download image file failed for "${url}"`, err)
|
Logger.error(`[CoverController] Download image file failed for "${url}"`, err)
|
||||||
return false
|
return false
|
||||||
@ -140,25 +134,24 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var imgtype = await this.checkFileIsValidImage(temppath)
|
var imgtype = await this.checkFileIsValidImage(temppath, true)
|
||||||
|
|
||||||
if (imgtype.error) {
|
if (imgtype.error) {
|
||||||
return imgtype
|
return imgtype
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverFilename = `cover.${imgtype.ext}`
|
var coverFilename = `cover.${imgtype.ext}`
|
||||||
var coverPath = Path.posix.join(relPath, coverFilename)
|
var coverFullPath = Path.posix.join(coverDirPath, coverFilename)
|
||||||
var coverFullPath = Path.posix.join(fullPath, coverFilename)
|
|
||||||
await fs.rename(temppath, coverFullPath)
|
await fs.rename(temppath, coverFullPath)
|
||||||
|
|
||||||
await this.removeOldCovers(fullPath, '.' + imgtype.ext)
|
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
|
||||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
|
Logger.info(`[CoverController] Downloaded libraryItem cover "${coverFullPath}" from url "${url}" for "${libraryItem.media.metadata.title}"`)
|
||||||
|
|
||||||
audiobook.updateBookCover(coverPath, coverFullPath)
|
libraryItem.updateMediaCover(coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverPath
|
cover: coverFullPath
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error)
|
Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error)
|
||||||
@ -167,5 +160,94 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateCoverPath(coverPath, libraryItem) {
|
||||||
|
// Invalid cover path
|
||||||
|
if (!coverPath || coverPath.startsWith('http:') || coverPath.startsWith('https:')) {
|
||||||
|
Logger.error(`[CoverController] validate cover path invalid http url "${coverPath}"`)
|
||||||
|
return {
|
||||||
|
error: 'Invalid cover path'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coverPath = coverPath.replace(/\\/g, '/')
|
||||||
|
// Cover path already set on media
|
||||||
|
if (libraryItem.media.coverPath == coverPath) {
|
||||||
|
Logger.debug(`[CoverController] validate cover path already set "${coverPath}"`)
|
||||||
|
return {
|
||||||
|
cover: coverPath,
|
||||||
|
updated: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cover path does not exist
|
||||||
|
if (!await fs.pathExists(coverPath)) {
|
||||||
|
Logger.error(`[CoverController] validate cover path does not exist "${coverPath}"`)
|
||||||
|
return {
|
||||||
|
error: 'Cover path does not exist'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check valid image at path
|
||||||
|
var imgtype = await this.checkFileIsValidImage(coverPath, true)
|
||||||
|
if (imgtype.error) {
|
||||||
|
return imgtype
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverDirPath = this.getCoverDirectory(libraryItem)
|
||||||
|
|
||||||
|
// Cover path is not in correct directory - make a copy
|
||||||
|
if (!coverPath.startsWith(coverDirPath)) {
|
||||||
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
|
var coverFilename = `cover.${imgtype.ext}`
|
||||||
|
var newCoverPath = Path.posix.join(coverDirPath, coverFilename)
|
||||||
|
Logger.debug(`[CoverController] validate cover path copy cover from "${coverPath}" to "${newCoverPath}"`)
|
||||||
|
|
||||||
|
var copySuccess = await fs.copy(coverPath, newCoverPath, { overwrite: true }).then(() => true).catch((error) => {
|
||||||
|
Logger.error(`[CoverController] validate cover path failed to copy cover`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!copySuccess) {
|
||||||
|
return {
|
||||||
|
error: 'Failed to copy cover to dir'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await filePerms.setDefault(newCoverPath)
|
||||||
|
await this.removeOldCovers(coverDirPath, '.' + imgtype.ext)
|
||||||
|
Logger.debug(`[CoverController] cover copy success`)
|
||||||
|
coverPath = newCoverPath
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|
||||||
|
libraryItem.updateMediaCover(coverPath)
|
||||||
|
return {
|
||||||
|
cover: coverPath,
|
||||||
|
updated: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEmbeddedCoverArt(libraryItem) {
|
||||||
|
var audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
|
||||||
|
if (!audioFileWithCover) return false
|
||||||
|
|
||||||
|
var coverDirPath = this.getCoverDirectory(libraryItem)
|
||||||
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
|
var coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
|
||||||
|
var coverFilePath = Path.join(coverDirPath, coverFilename)
|
||||||
|
|
||||||
|
var coverAlreadyExists = await fs.pathExists(coverFilePath)
|
||||||
|
if (coverAlreadyExists) {
|
||||||
|
Logger.warn(`[Audiobook] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
||||||
|
if (success) {
|
||||||
|
libraryItem.updateMediaCover(coverFilePath)
|
||||||
|
return coverFilePath
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
module.exports = CoverController
|
module.exports = CoverController
|
49
server/Db.js
49
server/Db.js
@ -185,19 +185,56 @@ class Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateLibraryItem(libraryItem) {
|
async updateLibraryItem(libraryItem) {
|
||||||
if (libraryItem && libraryItem.saveMetadata) {
|
return this.updateLibraryItems([libraryItem])
|
||||||
await libraryItem.saveMetadata()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return this.libraryItemsDb.update((record) => record.id === libraryItem.id, () => libraryItem).then((results) => {
|
async updateLibraryItems(libraryItems) {
|
||||||
Logger.debug(`[DB] Library Item updated ${results.updated}`)
|
await Promise.all(libraryItems.map(async (li) => {
|
||||||
|
if (li && li.saveMetadata) return li.saveMetadata()
|
||||||
|
return null
|
||||||
|
}))
|
||||||
|
|
||||||
|
var libraryItemIds = libraryItems.map(li => li.id)
|
||||||
|
return this.libraryItemsDb.update((record) => libraryItemIds.includes(record.id), (record) => {
|
||||||
|
return libraryItems.find(li => li.id === record.id)
|
||||||
|
}).then((results) => {
|
||||||
|
Logger.debug(`[DB] Library Items updated ${results.updated}`)
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Library Item update failed ${error}`)
|
Logger.error(`[DB] Library Items update failed ${error}`)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async insertLibraryItem(libraryItem) {
|
||||||
|
return this.insertLibraryItems([libraryItem])
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertLibraryItems(libraryItems) {
|
||||||
|
await Promise.all(libraryItems.map(async (li) => {
|
||||||
|
if (li && li.saveMetadata) return li.saveMetadata()
|
||||||
|
return null
|
||||||
|
}))
|
||||||
|
|
||||||
|
return this.libraryItemsDb.insert(libraryItems).then((results) => {
|
||||||
|
Logger.debug(`[DB] Library Items inserted ${results.inserted}`)
|
||||||
|
this.libraryItems = this.libraryItems.concat(libraryItems)
|
||||||
|
return true
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[DB] Library Items insert failed ${error}`)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLibraryItem(id) {
|
||||||
|
return this.libraryItemsDb.delete((record) => record.id === id).then((results) => {
|
||||||
|
Logger.debug(`[DB] Deleted Library Items: ${results.deleted}`)
|
||||||
|
this.libraryItems = this.libraryItems.filter(li => li.id !== id)
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[DB] Remove Library Items Failed: ${error}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async updateAudiobook(audiobook) {
|
async updateAudiobook(audiobook) {
|
||||||
if (audiobook && audiobook.saveAbMetadata) {
|
if (audiobook && audiobook.saveAbMetadata) {
|
||||||
// TODO: Book may have updates where this save is not necessary
|
// TODO: Book may have updates where this save is not necessary
|
||||||
|
@ -11,9 +11,7 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
|
|||||||
const { getFileSize } = require('./utils/fileUtils')
|
const { getFileSize } = require('./utils/fileUtils')
|
||||||
const TAG = 'DownloadManager'
|
const TAG = 'DownloadManager'
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
constructor(db, Uid, Gid) {
|
constructor(db) {
|
||||||
this.Uid = Uid
|
|
||||||
this.Gid = Gid
|
|
||||||
this.db = db
|
this.db = db
|
||||||
|
|
||||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||||
@ -344,7 +342,7 @@ class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set file permissions and ownership
|
// Set file permissions and ownership
|
||||||
await filePerms(download.fullPath, 0o774, this.Uid, this.Gid)
|
await filePerms.setDefault(download.fullPath)
|
||||||
|
|
||||||
var filesize = await getFileSize(download.fullPath)
|
var filesize = await getFileSize(download.fullPath)
|
||||||
download.setComplete(filesize)
|
download.setComplete(filesize)
|
||||||
|
@ -32,11 +32,9 @@ const CacheManager = require('./CacheManager')
|
|||||||
class Server {
|
class Server {
|
||||||
constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||||
this.Port = PORT
|
this.Port = PORT
|
||||||
this.Uid = isNaN(UID) ? 0 : Number(UID)
|
|
||||||
this.Gid = isNaN(GID) ? 0 : Number(GID)
|
|
||||||
this.Host = '0.0.0.0'
|
this.Host = '0.0.0.0'
|
||||||
global.Uid = this.Uid
|
global.Uid = isNaN(UID) ? 0 : Number(UID)
|
||||||
global.Gid = this.Gid
|
global.Gid = isNaN(GID) ? 0 : Number(GID)
|
||||||
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||||
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||||
global.MetadataPath = Path.normalize(METADATA_PATH)
|
global.MetadataPath = Path.normalize(METADATA_PATH)
|
||||||
@ -53,7 +51,7 @@ class Server {
|
|||||||
|
|
||||||
this.db = new Db()
|
this.db = new Db()
|
||||||
this.auth = new Auth(this.db)
|
this.auth = new Auth(this.db)
|
||||||
this.backupManager = new BackupManager(this.Uid, this.Gid, this.db)
|
this.backupManager = new BackupManager(this.db)
|
||||||
this.logManager = new LogManager(this.db)
|
this.logManager = new LogManager(this.db)
|
||||||
this.cacheManager = new CacheManager()
|
this.cacheManager = new CacheManager()
|
||||||
this.watcher = new Watcher()
|
this.watcher = new Watcher()
|
||||||
@ -61,7 +59,7 @@ class Server {
|
|||||||
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
||||||
|
|
||||||
this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
this.downloadManager = new DownloadManager(this.db, this.Uid, this.Gid)
|
this.downloadManager = new DownloadManager(this.db)
|
||||||
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.streamManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.streamManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
|
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
|
||||||
|
|
||||||
@ -268,8 +266,8 @@ class Server {
|
|||||||
// Scanning
|
// Scanning
|
||||||
socket.on('scan', this.scan.bind(this))
|
socket.on('scan', this.scan.bind(this))
|
||||||
socket.on('cancel_scan', this.cancelScan.bind(this))
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||||
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
|
socket.on('scan_item', (libraryItemId) => this.scanLibraryItem(socket, libraryItemId))
|
||||||
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
|
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
|
||||||
|
|
||||||
// Streaming (only still used in the mobile app)
|
// Streaming (only still used in the mobile app)
|
||||||
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
||||||
@ -329,15 +327,15 @@ class Server {
|
|||||||
Logger.info('[Server] Scan complete')
|
Logger.info('[Server] Scan complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAudiobook(socket, audiobookId) {
|
async scanLibraryItem(socket, libraryItemId) {
|
||||||
var result = await this.scanner.scanAudiobookById(audiobookId)
|
var result = await this.scanner.scanLibraryItemById(libraryItemId)
|
||||||
var scanResultName = ''
|
var scanResultName = ''
|
||||||
for (const key in ScanResult) {
|
for (const key in ScanResult) {
|
||||||
if (ScanResult[key] === result) {
|
if (ScanResult[key] === result) {
|
||||||
scanResultName = key
|
scanResultName = key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
socket.emit('audiobook_scan_complete', scanResultName)
|
socket.emit('item_scan_complete', scanResultName)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelScan(id) {
|
cancelScan(id) {
|
||||||
@ -459,8 +457,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info(`[Server] Setting owner/perms for first dir "${firstDirPath}"`)
|
await filePerms.setDefault(firstDirPath)
|
||||||
await filePerms(firstDirPath, 0o774, this.Uid, this.Gid)
|
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
@ -57,12 +57,12 @@ class LibraryController {
|
|||||||
// Update watcher
|
// Update watcher
|
||||||
this.watcher.updateLibrary(library)
|
this.watcher.updateLibrary(library)
|
||||||
|
|
||||||
// Remove audiobooks no longer in library
|
// Remove libraryItems no longer in library
|
||||||
var audiobooksToRemove = this.db.audiobooks.filter(ab => ab.libraryId === library.id && !library.checkFullPathInLibrary(ab.fullPath))
|
var itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||||
if (audiobooksToRemove.length) {
|
if (itemsToRemove.length) {
|
||||||
Logger.info(`[Scanner] Updating library, removing ${audiobooksToRemove.length} audiobooks`)
|
Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`)
|
||||||
for (let i = 0; i < audiobooksToRemove.length; i++) {
|
for (let i = 0; i < itemsToRemove.length; i++) {
|
||||||
await this.handleDeleteAudiobook(audiobooksToRemove[i])
|
await this.handleDeleteLibraryItem(itemsToRemove[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('library', library)
|
await this.db.updateEntity('library', library)
|
||||||
@ -77,11 +77,11 @@ class LibraryController {
|
|||||||
// Remove library watcher
|
// Remove library watcher
|
||||||
this.watcher.removeLibrary(library)
|
this.watcher.removeLibrary(library)
|
||||||
|
|
||||||
// Remove audiobooks in this library
|
// Remove items in this library
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
var libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
||||||
Logger.info(`[Server] deleting library "${library.name}" with ${audiobooks.length} audiobooks"`)
|
Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`)
|
||||||
for (let i = 0; i < audiobooks.length; i++) {
|
for (let i = 0; i < libraryItems.length; i++) {
|
||||||
await this.handleDeleteAudiobook(audiobooks[i])
|
await this.handleDeleteLibraryItem(libraryItems[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryJson = library.toJSON()
|
var libraryJson = library.toJSON()
|
||||||
@ -91,7 +91,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/items
|
// api/libraries/:id/items
|
||||||
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
|
// TODO: Optimize this method, items are iterated through several times but can be combined
|
||||||
getLibraryItems(req, res) {
|
getLibraryItems(req, res) {
|
||||||
var libraryId = req.library.id
|
var libraryId = req.library.id
|
||||||
var media = req.query.media || 'all'
|
var media = req.query.media || 'all'
|
||||||
|
@ -12,10 +12,6 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to update without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var libraryItem = req.libraryItem
|
var libraryItem = req.libraryItem
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
// Item has cover and update is removing cover so purge it from cache
|
||||||
if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
|
if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
|
||||||
@ -31,15 +27,15 @@ class LibraryItemController {
|
|||||||
res.json(libraryItem.toJSON())
|
res.json(libraryItem.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(req, res) {
|
||||||
|
await this.handleDeleteLibraryItem(req.libraryItem)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// PATCH: will create new authors & series if in payload
|
// PATCH: will create new authors & series if in payload
|
||||||
//
|
//
|
||||||
async updateMedia(req, res) {
|
async updateMedia(req, res) {
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to update without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var libraryItem = req.libraryItem
|
var libraryItem = req.libraryItem
|
||||||
var mediaPayload = req.body
|
var mediaPayload = req.body
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
// Item has cover and update is removing cover so purge it from cache
|
||||||
@ -100,6 +96,75 @@ class LibraryItemController {
|
|||||||
res.json(libraryItem)
|
res.json(libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/items/: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 libraryItem = req.libraryItem
|
||||||
|
|
||||||
|
var result = null
|
||||||
|
if (req.body && req.body.url) {
|
||||||
|
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
||||||
|
result = await this.coverController.downloadCoverFromUrl(libraryItem, req.body.url)
|
||||||
|
} else if (req.files && req.files.cover) {
|
||||||
|
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
||||||
|
result = await this.coverController.uploadCover(libraryItem, req.files.cover)
|
||||||
|
} 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.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
cover: result.cover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH: api/items/:id/cover
|
||||||
|
async updateCover(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
if (!req.body.cover) {
|
||||||
|
return res.status(400).error('Invalid request no cover path')
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = await this.coverController.validateCoverPath(req.body.cover, libraryItem)
|
||||||
|
if (validationResult.error) {
|
||||||
|
return res.status(500).send(validationResult.error)
|
||||||
|
}
|
||||||
|
if (validationResult.updated) {
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
cover: validationResult.cover
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: api/items/:id/cover
|
||||||
|
async removeCover(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
|
||||||
|
if (libraryItem.media.coverPath) {
|
||||||
|
libraryItem.updateMediaCover('')
|
||||||
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
// GET api/items/:id/cover
|
// GET api/items/:id/cover
|
||||||
async getCover(req, res) {
|
async getCover(req, res) {
|
||||||
let { query: { width, height, format }, libraryItem } = req
|
let { query: { width, height, format }, libraryItem } = req
|
||||||
@ -114,13 +179,21 @@ class LibraryItemController {
|
|||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media || !item.media.coverPath) return res.sendStatus(404)
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this audiobooks library
|
// Check user can access this audiobooks library
|
||||||
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
|
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||||
|
Logger.warn('[LibraryItemController] User attempted to update without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
req.libraryItem = item
|
req.libraryItem = item
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const { version } = require('../../package.json')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const LibraryFile = require('./files/LibraryFile')
|
const LibraryFile = require('./files/LibraryFile')
|
||||||
const Book = require('./entities/Book')
|
const Book = require('./entities/Book')
|
||||||
@ -22,8 +23,10 @@ class LibraryItem {
|
|||||||
this.lastScan = null
|
this.lastScan = null
|
||||||
this.scanVersion = null
|
this.scanVersion = null
|
||||||
|
|
||||||
// Entity was scanned and not found
|
// Was scanned and no longer exists
|
||||||
this.isMissing = false
|
this.isMissing = false
|
||||||
|
// Was scanned and no longer has media files
|
||||||
|
this.isInvalid = false
|
||||||
|
|
||||||
this.mediaType = null
|
this.mediaType = null
|
||||||
this.media = null
|
this.media = null
|
||||||
@ -51,6 +54,7 @@ class LibraryItem {
|
|||||||
this.scanVersion = libraryItem.scanVersion || null
|
this.scanVersion = libraryItem.scanVersion || null
|
||||||
|
|
||||||
this.isMissing = !!libraryItem.isMissing
|
this.isMissing = !!libraryItem.isMissing
|
||||||
|
this.isInvalid = !!libraryItem.isInvalid
|
||||||
|
|
||||||
this.mediaType = libraryItem.mediaType
|
this.mediaType = libraryItem.mediaType
|
||||||
if (this.mediaType === 'book') {
|
if (this.mediaType === 'book') {
|
||||||
@ -78,6 +82,7 @@ class LibraryItem {
|
|||||||
lastScan: this.lastScan,
|
lastScan: this.lastScan,
|
||||||
scanVersion: this.scanVersion,
|
scanVersion: this.scanVersion,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toJSON(),
|
media: this.media.toJSON(),
|
||||||
libraryFiles: this.libraryFiles.map(f => f.toJSON())
|
libraryFiles: this.libraryFiles.map(f => f.toJSON())
|
||||||
@ -98,6 +103,7 @@ class LibraryItem {
|
|||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toJSONMinified(),
|
media: this.media.toJSONMinified(),
|
||||||
numFiles: this.libraryFiles.length
|
numFiles: this.libraryFiles.length
|
||||||
@ -121,6 +127,7 @@ class LibraryItem {
|
|||||||
lastScan: this.lastScan,
|
lastScan: this.lastScan,
|
||||||
scanVersion: this.scanVersion,
|
scanVersion: this.scanVersion,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toJSONExpanded(),
|
media: this.media.toJSONExpanded(),
|
||||||
libraryFiles: this.libraryFiles.map(f => f.toJSON()),
|
libraryFiles: this.libraryFiles.map(f => f.toJSON()),
|
||||||
@ -133,6 +140,42 @@ class LibraryItem {
|
|||||||
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
|
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
get hasAudioFiles() {
|
||||||
|
return this.libraryFiles.some(lf => lf.fileType === 'audio')
|
||||||
|
}
|
||||||
|
get hasMediaFiles() {
|
||||||
|
return this.media.hasMediaFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data comes from scandir library item data
|
||||||
|
setData(libraryMediaType, payload) {
|
||||||
|
if (libraryMediaType === 'podcast') {
|
||||||
|
this.mediaType = 'podcast'
|
||||||
|
this.media = new Podcast()
|
||||||
|
} else {
|
||||||
|
this.mediaType = 'book'
|
||||||
|
this.media = new Book()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in payload) {
|
||||||
|
if (key === 'libraryFiles') {
|
||||||
|
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
|
||||||
|
|
||||||
|
// Use first image library file as cover
|
||||||
|
var firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
|
||||||
|
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
|
||||||
|
} else if (this[key] !== undefined) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.mediaMetadata) {
|
||||||
|
this.media.setData(payload.mediaMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addedAt = Date.now()
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
@ -149,7 +192,214 @@ class LibraryItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasUpdates) {
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMediaCover(coverPath) {
|
||||||
|
this.media.updateCover(coverPath)
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
setMissing() {
|
||||||
|
this.isMissing = true
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvalid() {
|
||||||
|
this.isInvalid = true
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastScan() {
|
||||||
|
this.lastScan = Date.now()
|
||||||
|
this.scanVersion = version
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMetadata() { }
|
||||||
|
|
||||||
|
// Returns null if file not found, true if file was updated, false if up to date
|
||||||
|
checkFileFound(fileFound) {
|
||||||
|
var hasUpdated = false
|
||||||
|
|
||||||
|
var existingFile = this.libraryFiles.find(lf => lf.ino === fileFound.ino)
|
||||||
|
var mediaFile = null
|
||||||
|
if (!existingFile) {
|
||||||
|
existingFile = this.libraryFiles.find(lf => lf.metadata.path === fileFound.metadata.path)
|
||||||
|
if (existingFile) {
|
||||||
|
// Update media file ino
|
||||||
|
mediaFile = this.media.findFileWithInode(existingFile.ino)
|
||||||
|
if (mediaFile) {
|
||||||
|
mediaFile.ino = fileFound.ino
|
||||||
|
}
|
||||||
|
|
||||||
|
// file inode was updated
|
||||||
|
existingFile.ino = fileFound.ino
|
||||||
|
hasUpdated = true
|
||||||
|
} else {
|
||||||
|
// file not found
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mediaFile = this.media.findFileWithInode(existingFile.ino)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingFile.metadata.path !== fileFound.metadata.path) {
|
||||||
|
existingFile.metadata.path = fileFound.metadata.path
|
||||||
|
existingFile.metadata.relPath = fileFound.metadata.relPath
|
||||||
|
if (mediaFile) {
|
||||||
|
mediaFile.metadata.path = fileFound.metadata.path
|
||||||
|
mediaFile.metadata.relPath = fileFound.metadata.relPath
|
||||||
|
}
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
|
||||||
|
keysToCheck.forEach((key) => {
|
||||||
|
if (existingFile.metadata[key] !== fileFound.metadata[key]) {
|
||||||
|
|
||||||
|
// Add modified flag on file data object if exists and was changed
|
||||||
|
if (key === 'mtimeMs' && existingFile.metadata[key]) {
|
||||||
|
fileFound.metadata.wasModified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
existingFile.metadata[key] = fileFound.metadata[key]
|
||||||
|
if (mediaFile) {
|
||||||
|
if (key === 'mtimeMs') mediaFile.metadata.wasModified = true
|
||||||
|
mediaFile.metadata[key] = fileFound.metadata[key]
|
||||||
|
}
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return hasUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data pulled from scandir during a scan, check it with current data
|
||||||
|
checkScanData(dataFound) {
|
||||||
|
var hasUpdated = false
|
||||||
|
|
||||||
|
if (this.isMissing) {
|
||||||
|
// Item no longer missing
|
||||||
|
this.isMissing = false
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataFound.ino !== this.ino) {
|
||||||
|
this.ino = dataFound.ino
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataFound.folderId !== this.folderId) {
|
||||||
|
Logger.warn(`[LibraryItem] Check scan item changed folder ${this.folderId} -> ${dataFound.folderId}`)
|
||||||
|
this.folderId = dataFound.folderId
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataFound.path !== this.path) {
|
||||||
|
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}"`)
|
||||||
|
this.path = dataFound.path
|
||||||
|
this.relPath = dataFound.relPath
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
|
||||||
|
keysToCheck.forEach((key) => {
|
||||||
|
if (dataFound[key] != this[key]) {
|
||||||
|
this[key] = dataFound[key] || 0
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var newLibraryFiles = []
|
||||||
|
var existingLibraryFiles = []
|
||||||
|
|
||||||
|
dataFound.libraryFiles.forEach((lf) => {
|
||||||
|
var fileFoundCheck = this.checkFileFound(lf, true)
|
||||||
|
console.log('Check library file', fileFoundCheck, lf.metadata.filename)
|
||||||
|
if (fileFoundCheck === null) {
|
||||||
|
newLibraryFiles.push(lf)
|
||||||
|
} else if (fileFoundCheck) {
|
||||||
|
hasUpdated = true
|
||||||
|
existingLibraryFiles.push(lf)
|
||||||
|
} else {
|
||||||
|
existingLibraryFiles.push(lf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const filesRemoved = []
|
||||||
|
|
||||||
|
// Remove files not found (inodes will all be up to date at this point)
|
||||||
|
this.libraryFiles = this.libraryFiles.filter(lf => {
|
||||||
|
if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) {
|
||||||
|
if (lf.metadata.path === this.media.coverPath) {
|
||||||
|
Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`)
|
||||||
|
this.media.updateCover('')
|
||||||
|
}
|
||||||
|
filesRemoved.push(lf.toJSON())
|
||||||
|
this.media.removeFileWithInode(lf.ino)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if (filesRemoved.length) {
|
||||||
|
this.media.checkUpdateMissingTracks()
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add library files to library item
|
||||||
|
if (newLibraryFiles.length) {
|
||||||
|
newLibraryFiles.forEach((lf) => this.libraryFiles.push(lf.clone()))
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if invalid
|
||||||
|
this.isInvalid = !this.media.hasMediaFiles
|
||||||
|
|
||||||
|
// If cover path is in item folder, make sure libraryFile exists for it
|
||||||
|
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
|
||||||
|
var lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
|
||||||
|
if (!lf) {
|
||||||
|
Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
|
||||||
|
this.media.updateCover('')
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdated) {
|
||||||
|
this.setLastScan()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: hasUpdated,
|
||||||
|
newLibraryFiles,
|
||||||
|
filesRemoved,
|
||||||
|
existingLibraryFiles // Existing file data may get re-scanned if forceRescan is set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set metadata from files
|
||||||
|
async syncFiles(preferOpfMetadata) {
|
||||||
|
var imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
||||||
|
console.log('image files', imageFiles.length, 'has cover', this.media.coverPath)
|
||||||
|
if (imageFiles.length && !this.media.coverPath) {
|
||||||
|
this.media.coverPath = imageFiles[0].metadata.path
|
||||||
|
Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
|
||||||
|
if (!textMetadataFiles.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasUpdated = await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)
|
||||||
|
if (hasUpdated) {
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
|
return hasUpdated
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
@ -53,5 +53,10 @@ class Author {
|
|||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkNameEquals(name) {
|
||||||
|
if (!name) return false
|
||||||
|
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Author
|
module.exports = Author
|
@ -2,7 +2,10 @@ const Logger = require('../../Logger')
|
|||||||
const BookMetadata = require('../metadata/BookMetadata')
|
const BookMetadata = require('../metadata/BookMetadata')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const EBookFile = require('../files/EBookFile')
|
const EBookFile = require('../files/EBookFile')
|
||||||
|
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
|
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
||||||
|
const { readTextFile } = require('../../utils/fileUtils')
|
||||||
|
|
||||||
class Book {
|
class Book {
|
||||||
constructor(book) {
|
constructor(book) {
|
||||||
@ -13,6 +16,10 @@ class Book {
|
|||||||
this.audioFiles = []
|
this.audioFiles = []
|
||||||
this.ebookFiles = []
|
this.ebookFiles = []
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
|
this.missingParts = []
|
||||||
|
|
||||||
|
this.lastCoverSearch = null
|
||||||
|
this.lastCoverSearchQuery = null
|
||||||
|
|
||||||
if (book) {
|
if (book) {
|
||||||
this.construct(book)
|
this.construct(book)
|
||||||
@ -26,6 +33,9 @@ class Book {
|
|||||||
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
||||||
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
|
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
|
||||||
this.chapters = book.chapters.map(c => ({ ...c }))
|
this.chapters = book.chapters.map(c => ({ ...c }))
|
||||||
|
this.missingParts = book.missingParts ? [...book.missingParts] : []
|
||||||
|
this.lastCoverSearch = book.lastCoverSearch || null
|
||||||
|
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -35,7 +45,8 @@ class Book {
|
|||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||||
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c }))
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
missingParts: [...this.missingParts]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +59,7 @@ class Book {
|
|||||||
numAudioFiles: this.audioFiles.length,
|
numAudioFiles: this.audioFiles.length,
|
||||||
numEbooks: this.ebookFiles.length,
|
numEbooks: this.ebookFiles.length,
|
||||||
numChapters: this.chapters.length,
|
numChapters: this.chapters.length,
|
||||||
|
numMissingParts: this.missingParts.length,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
size: this.size
|
size: this.size
|
||||||
}
|
}
|
||||||
@ -63,7 +75,8 @@ class Book {
|
|||||||
chapters: this.chapters.map(c => ({ ...c })),
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
tracks: this.tracks.map(t => t.toJSON())
|
tracks: this.tracks.map(t => t.toJSON()),
|
||||||
|
missingParts: [...this.missingParts]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +93,17 @@ class Book {
|
|||||||
this.audioFiles.forEach((af) => total += af.metadata.size)
|
this.audioFiles.forEach((af) => total += af.metadata.size)
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
get hasMediaFiles() {
|
||||||
|
return !!(this.tracks.length + this.ebookFiles.length)
|
||||||
|
}
|
||||||
|
get shouldSearchForCover() {
|
||||||
|
if (this.coverPath) return false
|
||||||
|
if (!this.lastCoverSearch || this.metadata.coverSearchQuery !== this.lastCoverSearchQuery) return true
|
||||||
|
return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day
|
||||||
|
}
|
||||||
|
get hasEmbeddedCoverArt() {
|
||||||
|
return this.audioFiles.some(af => af.embeddedCoverArt)
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
@ -99,5 +123,195 @@ class Book {
|
|||||||
}
|
}
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCover(coverPath) {
|
||||||
|
coverPath = coverPath.replace(/\\/g, '/')
|
||||||
|
if (this.coverPath === coverPath) return false
|
||||||
|
this.coverPath = coverPath
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUpdateMissingTracks() {
|
||||||
|
var currMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
|
|
||||||
|
var current_index = 1
|
||||||
|
var missingParts = []
|
||||||
|
|
||||||
|
for (let i = 0; i < this.tracks.length; i++) {
|
||||||
|
var _track = this.tracks[i]
|
||||||
|
if (_track.index > current_index) {
|
||||||
|
var num_parts_missing = _track.index - current_index
|
||||||
|
for (let x = 0; x < num_parts_missing && x < 9999; x++) {
|
||||||
|
missingParts.push(current_index + x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_index = _track.index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.missingParts = missingParts
|
||||||
|
|
||||||
|
var newMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
|
var wasUpdated = newMissingParts !== currMissingParts
|
||||||
|
if (wasUpdated && this.missingParts.length) {
|
||||||
|
Logger.info(`[Book] "${this.metadata.title}" has ${missingParts.length} missing parts`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFileWithInode(inode) {
|
||||||
|
if (this.audioFiles.some(af => af.ino === inode)) {
|
||||||
|
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.ebookFiles.some(ef => ef.ino === inode)) {
|
||||||
|
this.ebookFiles = this.ebookFiles.filter(ef => ef.ino !== inode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
findFileWithInode(inode) {
|
||||||
|
var audioFile = this.audioFiles.find(af => af.ino == inode)
|
||||||
|
if (audioFile) return audioFile
|
||||||
|
var ebookFile = this.ebookFiles.find(ef => ef.inode == inode)
|
||||||
|
if (ebookFile) return ebookFile
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLastCoverSearch(coverWasFound) {
|
||||||
|
this.lastCoverSearch = coverWasFound ? null : Date.now()
|
||||||
|
this.lastCoverSearchQuery = coverWasFound ? null : this.metadata.coverSearchQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio file metadata tags map to book details (will not overwrite)
|
||||||
|
setMetadataFromAudioFile(overrideExistingDetails = false) {
|
||||||
|
if (!this.audioFiles.length) return false
|
||||||
|
var audioFile = this.audioFiles[0]
|
||||||
|
if (!audioFile.metaTags) return false
|
||||||
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildTracks() {
|
||||||
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
|
this.missingParts = []
|
||||||
|
this.setChapters()
|
||||||
|
this.checkUpdateMissingTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
setChapters() {
|
||||||
|
// If 1 audio file without chapters, then no chapters will be set
|
||||||
|
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
|
if (includedAudioFiles.length === 1) {
|
||||||
|
// 1 audio file with chapters
|
||||||
|
if (includedAudioFiles[0].chapters) {
|
||||||
|
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.chapters = []
|
||||||
|
var currChapterId = 0
|
||||||
|
var currStartTime = 0
|
||||||
|
includedAudioFiles.forEach((file) => {
|
||||||
|
// If audio file has chapters use chapters
|
||||||
|
if (file.chapters && file.chapters.length) {
|
||||||
|
file.chapters.forEach((chapter) => {
|
||||||
|
var chapterDuration = chapter.end - chapter.start
|
||||||
|
if (chapterDuration > 0) {
|
||||||
|
var title = `Chapter ${currChapterId}`
|
||||||
|
if (chapter.title) {
|
||||||
|
title += ` (${chapter.title})`
|
||||||
|
}
|
||||||
|
this.chapters.push({
|
||||||
|
id: currChapterId++,
|
||||||
|
start: currStartTime,
|
||||||
|
end: currStartTime + chapterDuration,
|
||||||
|
title
|
||||||
|
})
|
||||||
|
currStartTime += chapterDuration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (file.duration) {
|
||||||
|
// Otherwise just use track has chapter
|
||||||
|
this.chapters.push({
|
||||||
|
id: currChapterId++,
|
||||||
|
start: currStartTime,
|
||||||
|
end: currStartTime + file.duration,
|
||||||
|
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
|
||||||
|
})
|
||||||
|
currStartTime += file.duration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(scanMediaMetadata) {
|
||||||
|
this.metadata = new BookMetadata()
|
||||||
|
this.metadata.setData(scanMediaMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||||
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
|
var metadataUpdatePayload = {}
|
||||||
|
|
||||||
|
var descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
||||||
|
if (descTxt) {
|
||||||
|
var descriptionText = await readTextFile(descTxt.metadata.path)
|
||||||
|
if (descriptionText) {
|
||||||
|
Logger.debug(`[Book] "${this.metadata.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
|
||||||
|
metadataUpdatePayload.description = descriptionText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var readerTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'reader.txt')
|
||||||
|
if (readerTxt) {
|
||||||
|
var narratorText = await readTextFile(readerTxt.metadata.path)
|
||||||
|
if (narratorText) {
|
||||||
|
Logger.debug(`[Book] "${this.metadata.title}" found reader.txt updating narrator with "${narratorText}"`)
|
||||||
|
metadataUpdatePayload.narrators = this.metadata.parseNarratorsTag(narratorText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement metadata.abs
|
||||||
|
var metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
||||||
|
if (metadataAbs) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataOpf = textMetadataFiles.find(lf => lf.isOPFFile || lf.metadata.filename === 'metadata.xml')
|
||||||
|
if (metadataOpf) {
|
||||||
|
var xmlText = await readTextFile(metadataOpf.metadata.path)
|
||||||
|
if (xmlText) {
|
||||||
|
var opfMetadata = await parseOpfMetadataXML(xmlText)
|
||||||
|
if (opfMetadata) {
|
||||||
|
for (const key in opfMetadata) {
|
||||||
|
// Add genres only if genres are empty
|
||||||
|
if (key === 'genres') {
|
||||||
|
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
||||||
|
metadataUpdatePayload[key] = opfMetadata.genres
|
||||||
|
}
|
||||||
|
} else if (key === 'author') {
|
||||||
|
if (opfMetadata.author && (!this.metadata.authors.length || opfMetadataOverrideDetails)) {
|
||||||
|
metadataUpdatePayload.authors = this.metadata.parseAuthorsTag(opfMetadata.author)
|
||||||
|
}
|
||||||
|
} else if (key === 'narrator') {
|
||||||
|
if (opfMetadata.narrator && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
|
||||||
|
metadataUpdatePayload.narrators = this.metadata.parseNarratorsTag(opfMetadata.narrator)
|
||||||
|
}
|
||||||
|
} else if (key === 'series') {
|
||||||
|
if (opfMetadata.series && (!this.metadata.series.length || opfMetadataOverrideDetails)) {
|
||||||
|
metadataUpdatePayload.series = this.metadata.parseSeriesTag(opfMetadata.series, opfMetadata.sequence)
|
||||||
|
}
|
||||||
|
} else if (opfMetadata[key] && ((!this.metadata[key] && !metadataUpdatePayload[key]) || opfMetadataOverrideDetails)) {
|
||||||
|
metadataUpdatePayload[key] = opfMetadata[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(metadataUpdatePayload).length) {
|
||||||
|
return this.metadata.update(metadataUpdatePayload)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
@ -1,5 +1,6 @@
|
|||||||
const PodcastEpisode = require('./PodcastEpisode')
|
const PodcastEpisode = require('./PodcastEpisode')
|
||||||
const PodcastMetadata = require('../metadata/PodcastMetadata')
|
const PodcastMetadata = require('../metadata/PodcastMetadata')
|
||||||
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
|
|
||||||
class Podcast {
|
class Podcast {
|
||||||
constructor(podcast) {
|
constructor(podcast) {
|
||||||
@ -10,8 +11,8 @@ class Podcast {
|
|||||||
this.tags = []
|
this.tags = []
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
|
|
||||||
this.createdAt = null
|
this.lastCoverSearch = null
|
||||||
this.lastUpdate = null
|
this.lastCoverSearchQuery = null
|
||||||
|
|
||||||
if (podcast) {
|
if (podcast) {
|
||||||
this.construct(podcast)
|
this.construct(podcast)
|
||||||
@ -24,8 +25,6 @@ class Podcast {
|
|||||||
this.coverPath = podcast.coverPath
|
this.coverPath = podcast.coverPath
|
||||||
this.tags = [...podcast.tags]
|
this.tags = [...podcast.tags]
|
||||||
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
||||||
this.createdAt = podcast.createdAt
|
|
||||||
this.lastUpdate = podcast.lastUpdate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -35,8 +34,6 @@ class Podcast {
|
|||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
createdAt: this.createdAt,
|
|
||||||
lastUpdate: this.lastUpdate
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,8 +44,7 @@ class Podcast {
|
|||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
createdAt: this.createdAt,
|
|
||||||
lastUpdate: this.lastUpdate
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,9 +55,74 @@ class Podcast {
|
|||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
createdAt: this.createdAt,
|
|
||||||
lastUpdate: this.lastUpdate
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get tracks() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
get duration() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
get size() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
get hasMediaFiles() {
|
||||||
|
return !!this.episodes.length
|
||||||
|
}
|
||||||
|
get shouldSearchForCover() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
get hasEmbeddedCoverArt() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
update(payload) {
|
||||||
|
var json = this.toJSON()
|
||||||
|
var hasUpdates = false
|
||||||
|
for (const key in json) {
|
||||||
|
if (payload[key] !== undefined) {
|
||||||
|
if (key === 'metadata') {
|
||||||
|
if (this.metadata.update(payload.metadata)) {
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
} else if (!areEquivalent(payload[key], json[key])) {
|
||||||
|
this[key] = copyValue(payload[key])
|
||||||
|
Logger.debug('[Podcast] Key updated', key, this[key])
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCover(coverPath) {
|
||||||
|
coverPath = coverPath.replace(/\\/g, '/')
|
||||||
|
if (this.coverPath === coverPath) return false
|
||||||
|
this.coverPath = coverPath
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUpdateMissingTracks() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFileWithInode(inode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
findFileWithInode(inode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(scanMediaMetadata) {
|
||||||
|
this.metadata = new PodcastMetadata()
|
||||||
|
this.metadata.setData(scanMediaMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
@ -42,5 +42,10 @@ class Series {
|
|||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkNameEquals(name) {
|
||||||
|
if (!name) return false
|
||||||
|
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Series
|
module.exports = Series
|
@ -72,6 +72,9 @@ class AudioFile {
|
|||||||
this.index = data.index
|
this.index = data.index
|
||||||
this.ino = data.ino
|
this.ino = data.ino
|
||||||
this.metadata = new FileMetadata(data.metadata || {})
|
this.metadata = new FileMetadata(data.metadata || {})
|
||||||
|
if (!this.metadata.toJSON) {
|
||||||
|
console.error('No metadata tojosnm\n\n\n\n\n\n', this)
|
||||||
|
}
|
||||||
this.addedAt = data.addedAt
|
this.addedAt = data.addedAt
|
||||||
this.updatedAt = data.updatedAt
|
this.updatedAt = data.updatedAt
|
||||||
this.manuallyVerified = !!data.manuallyVerified
|
this.manuallyVerified = !!data.manuallyVerified
|
||||||
@ -101,19 +104,13 @@ class AudioFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New scanner creates AudioFile from AudioFileScanner
|
// New scanner creates AudioFile from AudioFileScanner
|
||||||
setDataFromProbe(fileData, probeData) {
|
setDataFromProbe(libraryFile, probeData) {
|
||||||
this.index = fileData.index || null
|
this.ino = libraryFile.ino || null
|
||||||
this.ino = fileData.ino || null
|
|
||||||
|
|
||||||
// TODO: Update file metadata for set data from probe
|
this.metadata = libraryFile.metadata.clone()
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
|
|
||||||
this.trackNumFromMeta = fileData.trackNumFromMeta
|
|
||||||
this.discNumFromMeta = fileData.discNumFromMeta
|
|
||||||
this.trackNumFromFilename = fileData.trackNumFromFilename
|
|
||||||
this.discNumFromFilename = fileData.discNumFromFilename
|
|
||||||
|
|
||||||
this.format = probeData.format
|
this.format = probeData.format
|
||||||
this.duration = probeData.duration
|
this.duration = probeData.duration
|
||||||
this.bitRate = probeData.bitRate || null
|
this.bitRate = probeData.bitRate || null
|
||||||
@ -196,9 +193,13 @@ class AudioFile {
|
|||||||
newjson.addedAt = this.addedAt
|
newjson.addedAt = this.addedAt
|
||||||
|
|
||||||
for (const key in newjson) {
|
for (const key in newjson) {
|
||||||
if (key === 'metaTags') {
|
if (key === 'metadata') {
|
||||||
if (!this.metaTags || !this.metaTags.isEqual(scannedAudioFile.metadata)) {
|
if (this.metadata.update(newjson[key])) {
|
||||||
this.metaTags = scannedAudioFile.metadata
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
} else if (key === 'metaTags') {
|
||||||
|
if (!this.metaTags || !this.metaTags.isEqual(scannedAudioFile.metaTags)) {
|
||||||
|
this.metaTags = scannedAudioFile.metaTags.clone()
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
} else if (key === 'chapters') {
|
} else if (key === 'chapters') {
|
||||||
@ -206,7 +207,6 @@ class AudioFile {
|
|||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
} else if (this[key] !== newjson[key]) {
|
} else if (this[key] !== newjson[key]) {
|
||||||
// console.log(this.filename, 'key', key, 'updated', this[key], newjson[key])
|
|
||||||
this[key] = newjson[key]
|
this[key] = newjson[key]
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const { getFileTimestampsWithIno } = require('../../utils/fileUtils')
|
||||||
const globals = require('../../utils/globals')
|
const globals = require('../../utils/globals')
|
||||||
const FileMetadata = require('../metadata/FileMetadata')
|
const FileMetadata = require('../metadata/FileMetadata')
|
||||||
|
|
||||||
@ -30,6 +32,10 @@ class LibraryFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new LibraryFile(this.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
get fileType() {
|
get fileType() {
|
||||||
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
|
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
|
||||||
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
|
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
|
||||||
@ -38,5 +44,27 @@ class LibraryFile {
|
|||||||
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
|
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isMediaFile() {
|
||||||
|
return this.fileType === 'audio' || this.fileType === 'ebook'
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOPFFile() {
|
||||||
|
return this.metadata.ext === '.opf'
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDataFromPath(path, relPath) {
|
||||||
|
var fileTsData = await getFileTimestampsWithIno(path)
|
||||||
|
var fileMetadata = new FileMetadata()
|
||||||
|
fileMetadata.setData(fileTsData)
|
||||||
|
fileMetadata.filename = Path.basename(relPath)
|
||||||
|
fileMetadata.path = path
|
||||||
|
fileMetadata.relPath = relPath
|
||||||
|
fileMetadata.ext = Path.extname(relPath)
|
||||||
|
this.ino = fileTsData.ino
|
||||||
|
this.metadata = fileMetadata
|
||||||
|
this.addedAt = Date.now()
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryFile
|
module.exports = LibraryFile
|
@ -1,6 +1,6 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const parseAuthors = require('../../utils/parseAuthors')
|
const parseAuthors = require('../../utils/parseNameString')
|
||||||
|
|
||||||
class Book {
|
class Book {
|
||||||
constructor(book = null) {
|
constructor(book = null) {
|
||||||
|
@ -118,6 +118,10 @@ class AudioMetaTags {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new AudioMetaTags(this.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
isEqual(audioFileMetadata) {
|
isEqual(audioFileMetadata) {
|
||||||
if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
|
if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
|
||||||
for (const key in audioFileMetadata.toJSON()) {
|
for (const key in audioFileMetadata.toJSON()) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
|
const parseNameString = require('../../utils/parseNameString')
|
||||||
class BookMetadata {
|
class BookMetadata {
|
||||||
constructor(metadata) {
|
constructor(metadata) {
|
||||||
this.title = null
|
this.title = null
|
||||||
@ -88,11 +88,16 @@ class BookMetadata {
|
|||||||
return this.title
|
return this.title
|
||||||
}
|
}
|
||||||
get authorName() {
|
get authorName() {
|
||||||
|
if (!this.authors.length) return ''
|
||||||
return this.authors.map(au => au.name).join(', ')
|
return this.authors.map(au => au.name).join(', ')
|
||||||
}
|
}
|
||||||
get narratorName() {
|
get narratorName() {
|
||||||
return this.narrators.join(', ')
|
return this.narrators.join(', ')
|
||||||
}
|
}
|
||||||
|
get coverSearchQuery() {
|
||||||
|
if (!this.authorName) return this.title
|
||||||
|
return this.title + '&' + this.authorName
|
||||||
|
}
|
||||||
|
|
||||||
hasAuthor(authorName) {
|
hasAuthor(authorName) {
|
||||||
return !!this.authors.find(au => au.name == authorName)
|
return !!this.authors.find(au => au.name == authorName)
|
||||||
@ -118,5 +123,150 @@ class BookMetadata {
|
|||||||
}
|
}
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setData(scanMediaData = {}) {
|
||||||
|
this.title = scanMediaData.title || null
|
||||||
|
this.subtitle = scanMediaData.subtitle || null
|
||||||
|
this.narrators = []
|
||||||
|
this.publishYear = scanMediaData.publishYear || null
|
||||||
|
this.description = scanMediaData.description || null
|
||||||
|
this.isbn = scanMediaData.isbn || null
|
||||||
|
this.asin = scanMediaData.asin || null
|
||||||
|
this.language = scanMediaData.language || null
|
||||||
|
this.genres = []
|
||||||
|
|
||||||
|
if (scanMediaData.author) {
|
||||||
|
this.authors = this.parseAuthorsTag(scanMediaData.author)
|
||||||
|
}
|
||||||
|
if (scanMediaData.series) {
|
||||||
|
this.series = this.parseSeriesTag(scanMediaData.series, scanMediaData.sequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
|
||||||
|
const MetadataMapArray = [
|
||||||
|
{
|
||||||
|
tag: 'tagComposer',
|
||||||
|
key: 'narrators'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDescription',
|
||||||
|
key: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagPublisher',
|
||||||
|
key: 'publisher'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDate',
|
||||||
|
key: 'publishYear'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagSubtitle',
|
||||||
|
key: 'subtitle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagAlbum',
|
||||||
|
altTag: 'tagTitle',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagArtist',
|
||||||
|
altTag: 'tagAlbumArtist',
|
||||||
|
key: 'authors'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagGenre',
|
||||||
|
key: 'genres'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagSeries',
|
||||||
|
key: 'series'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagIsbn',
|
||||||
|
key: 'isbn'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagLanguage',
|
||||||
|
key: 'language'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagASIN',
|
||||||
|
key: 'asin'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
var updatePayload = {}
|
||||||
|
|
||||||
|
// Metadata is only mapped to the book if it is empty
|
||||||
|
MetadataMapArray.forEach((mapping) => {
|
||||||
|
var value = audioFileMetaTags[mapping.tag]
|
||||||
|
var tagToUse = mapping.tag
|
||||||
|
if (!value && mapping.altTag) {
|
||||||
|
value = audioFileMetaTags[mapping.altTag]
|
||||||
|
tagToUse = mapping.altTag
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
if (mapping.key === 'narrators' && (!this.narrators.length || overrideExistingDetails)) {
|
||||||
|
updatePayload.narrators = this.parseNarratorsTag(value)
|
||||||
|
} else if (mapping.key === 'authors' && (!this.authors.length || overrideExistingDetails)) {
|
||||||
|
updatePayload.authors = this.parseAuthorsTag(value)
|
||||||
|
} else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) {
|
||||||
|
updatePayload.genres = this.parseGenresTag(value)
|
||||||
|
} else if (mapping.key === 'series' && (!this.series.length || overrideExistingDetails)) {
|
||||||
|
var sequenceTag = audioFileMetaTags.tagSeriesPart || null
|
||||||
|
updatePayload.series = this.parseSeriesTag(value, sequenceTag)
|
||||||
|
} else if (!this[mapping.key] || overrideExistingDetails) {
|
||||||
|
updatePayload[mapping.key] = value
|
||||||
|
// Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Object.keys(updatePayload).length) {
|
||||||
|
return this.update(updatePayload)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns array of names in First Last format
|
||||||
|
parseNarratorsTag(narratorsTag) {
|
||||||
|
var parsed = parseNameString(narratorsTag)
|
||||||
|
return parsed ? parsed.names : []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return array of authors minified with placeholder id
|
||||||
|
parseAuthorsTag(authorsTag) {
|
||||||
|
var parsed = parseNameString(authorsTag)
|
||||||
|
if (!parsed) return []
|
||||||
|
return parsed.map((au) => {
|
||||||
|
return {
|
||||||
|
id: `new-${Math.floor(Math.random() * 1000000)}`,
|
||||||
|
name: au
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
parseGenresTag(genreTag) {
|
||||||
|
if (!genreTag || !genreTag.length) return []
|
||||||
|
var separators = ['/', '//', ';']
|
||||||
|
for (let i = 0; i < separators.length; i++) {
|
||||||
|
if (genreTag.includes(separators[i])) {
|
||||||
|
return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [genreTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return array with series with placeholder id
|
||||||
|
parseSeriesTag(seriesTag, sequenceTag) {
|
||||||
|
if (!seriesTag) return []
|
||||||
|
return [{
|
||||||
|
id: `new-${Math.floor(Math.random() * 1000000)}`,
|
||||||
|
name: seriesTag,
|
||||||
|
sequence: sequenceTag || ''
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = BookMetadata
|
module.exports = BookMetadata
|
@ -12,6 +12,9 @@ class FileMetadata {
|
|||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.construct(metadata)
|
this.construct(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temp flag used in scans
|
||||||
|
this.wasModified = false
|
||||||
}
|
}
|
||||||
|
|
||||||
construct(metadata) {
|
construct(metadata) {
|
||||||
@ -46,5 +49,24 @@ class FileMetadata {
|
|||||||
if (!this.ext) return ''
|
if (!this.ext) return ''
|
||||||
return this.ext.slice(1)
|
return this.ext.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(payload) {
|
||||||
|
var hasUpdates = false
|
||||||
|
for (const key in payload) {
|
||||||
|
if (this[key] !== undefined && this[key] !== payload[key]) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(payload) {
|
||||||
|
for (const key in payload) {
|
||||||
|
if (this[key] !== undefined) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = FileMetadata
|
module.exports = FileMetadata
|
@ -9,9 +9,9 @@ const { LogLevel } = require('../utils/constants')
|
|||||||
class AudioFileScanner {
|
class AudioFileScanner {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
getTrackAndDiscNumberFromFilename(bookScanData, audioFileData) {
|
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
||||||
const { title, author, series, publishYear } = bookScanData
|
const { title, author, series, publishYear } = mediaMetadataFromScan
|
||||||
const { filename, path } = audioFileData
|
const { filename, path } = audioLibraryFile.metadata
|
||||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||||
|
|
||||||
// Remove title, author, series, and publishYear from filename if there
|
// Remove title, author, series, and publishYear from filename if there
|
||||||
@ -54,25 +54,23 @@ class AudioFileScanner {
|
|||||||
return Math.floor(total / results.length)
|
return Math.floor(total / results.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
async scan(audioFileData, bookScanData, verbose = false) {
|
async scan(audioLibraryFile, mediaMetadataFromScan, verbose = false) {
|
||||||
var probeStart = Date.now()
|
var probeStart = Date.now()
|
||||||
// Logger.debug(`[AudioFileScanner] Start Probe ${audioFileData.fullPath}`)
|
var probeData = await prober.probe(audioLibraryFile.metadata.path, verbose)
|
||||||
var probeData = await prober.probe(audioFileData.fullPath, verbose)
|
|
||||||
if (probeData.error) {
|
if (probeData.error) {
|
||||||
Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioFileData.fullPath}"`)
|
Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioLibraryFile.metadata.path}"`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// Logger.debug(`[AudioFileScanner] Finished Probe ${audioFileData.fullPath} elapsed ${msToTimestamp(Date.now() - probeStart, true)}`)
|
|
||||||
|
|
||||||
var audioFile = new AudioFile()
|
var audioFile = new AudioFile()
|
||||||
audioFileData.trackNumFromMeta = probeData.trackNumber
|
audioFile.trackNumFromMeta = probeData.trackNumber
|
||||||
audioFileData.discNumFromMeta = probeData.discNumber
|
audioFile.discNumFromMeta = probeData.discNumber
|
||||||
|
|
||||||
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(bookScanData, audioFileData)
|
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile)
|
||||||
audioFileData.trackNumFromFilename = trackNumber
|
audioFile.trackNumFromFilename = trackNumber
|
||||||
audioFileData.discNumFromFilename = discNumber
|
audioFile.discNumFromFilename = discNumber
|
||||||
|
|
||||||
audioFile.setDataFromProbe(audioFileData, probeData)
|
audioFile.setDataFromProbe(audioLibraryFile, probeData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audioFile,
|
audioFile,
|
||||||
@ -81,10 +79,11 @@ class AudioFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
|
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
|
||||||
async executeAudioFileScans(audioFileDataArray, bookScanData) {
|
async executeAudioFileScans(audioLibraryFiles, scanData) {
|
||||||
|
var mediaMetadataFromScan = scanData.mediaMetadata || null
|
||||||
var proms = []
|
var proms = []
|
||||||
for (let i = 0; i < audioFileDataArray.length; i++) {
|
for (let i = 0; i < audioLibraryFiles.length; i++) {
|
||||||
proms.push(this.scan(audioFileDataArray[i], bookScanData))
|
proms.push(this.scan(audioLibraryFiles[i], mediaMetadataFromScan))
|
||||||
}
|
}
|
||||||
var scanStart = Date.now()
|
var scanStart = Date.now()
|
||||||
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
||||||
@ -117,7 +116,7 @@ class AudioFileScanner {
|
|||||||
return nodupes
|
return nodupes
|
||||||
}
|
}
|
||||||
|
|
||||||
runSmartTrackOrder(audiobook, audioFiles) {
|
runSmartTrackOrder(libraryItem, audioFiles) {
|
||||||
var discsFromFilename = []
|
var discsFromFilename = []
|
||||||
var tracksFromFilename = []
|
var tracksFromFilename = []
|
||||||
var discsFromMeta = []
|
var discsFromMeta = []
|
||||||
@ -153,75 +152,78 @@ class AudioFileScanner {
|
|||||||
|
|
||||||
|
|
||||||
if (discKey !== null) {
|
if (discKey !== null) {
|
||||||
Logger.debug(`[AudioFileScanner] Smart track order for "${audiobook.title}" using disc key ${discKey} and track key ${trackKey}`)
|
Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using disc key ${discKey} and track key ${trackKey}`)
|
||||||
audioFiles.sort((a, b) => {
|
audioFiles.sort((a, b) => {
|
||||||
let Dx = a[discKey] - b[discKey]
|
let Dx = a[discKey] - b[discKey]
|
||||||
if (Dx === 0) Dx = a[trackKey] - b[trackKey]
|
if (Dx === 0) Dx = a[trackKey] - b[trackKey]
|
||||||
return Dx
|
return Dx
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[AudioFileScanner] Smart track order for "${audiobook.title}" using track key ${trackKey}`)
|
Logger.debug(`[AudioFileScanner] Smart track order for "${libraryItem.media.metadata.title}" using track key ${trackKey}`)
|
||||||
audioFiles.sort((a, b) => a[trackKey] - b[trackKey])
|
audioFiles.sort((a, b) => a[trackKey] - b[trackKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < audioFiles.length; i++) {
|
for (let i = 0; i < audioFiles.length; i++) {
|
||||||
audioFiles[i].index = i + 1
|
audioFiles[i].index = i + 1
|
||||||
var existingAF = audiobook.getAudioFileByIno(audioFiles[i].ino)
|
var existingAF = libraryItem.media.findFileWithInode(audioFiles[i].ino)
|
||||||
if (existingAF) {
|
if (existingAF) {
|
||||||
audiobook.updateAudioFile(audioFiles[i])
|
if (existingAF.updateFromScan) existingAF.updateFromScan(audioFiles[i])
|
||||||
} else {
|
} else {
|
||||||
audiobook.addAudioFile(audioFiles[i])
|
libraryItem.media.audioFiles.push(audioFiles[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAudioFiles(audioFileDataArray, bookScanData, audiobook, preferAudioMetadata, libraryScan = null) {
|
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var audioScanResult = await this.executeAudioFileScans(audioFileDataArray, bookScanData)
|
var audioScanResult = await this.executeAudioFileScans(audioLibraryFiles, scanData)
|
||||||
if (audioScanResult.audioFiles.length) {
|
if (audioScanResult.audioFiles.length) {
|
||||||
if (libraryScan) {
|
if (libraryScan) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Book "${bookScanData.path}" Audio file scan took ${audioScanResult.elapsed}ms for ${audioScanResult.audioFiles.length} with average time of ${audioScanResult.averageScanDuration}ms`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${audioScanResult.elapsed}ms for ${audioScanResult.audioFiles.length} with average time of ${audioScanResult.averageScanDuration}ms`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalAudioFilesToInclude = audioScanResult.audioFiles.length
|
var totalAudioFilesToInclude = audioScanResult.audioFiles.length
|
||||||
var newAudioFiles = audioScanResult.audioFiles.filter(af => {
|
var newAudioFiles = audioScanResult.audioFiles.filter(af => {
|
||||||
return !audiobook.audioFilesToInclude.find(_af => _af.ino === af.ino)
|
return !libraryItem.libraryFiles.find(lf => lf.ino === af.ino)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newAudioFiles.length) {
|
// Adding audio files to book media
|
||||||
// Single Track Audiobooks
|
if (libraryItem.mediaType === 'book') {
|
||||||
if (totalAudioFilesToInclude === 1) {
|
if (newAudioFiles.length) {
|
||||||
var af = audioScanResult.audioFiles[0]
|
// Single Track Audiobooks
|
||||||
af.index = 1
|
if (totalAudioFilesToInclude === 1) {
|
||||||
audiobook.addAudioFile(af)
|
var af = audioScanResult.audioFiles[0]
|
||||||
hasUpdated = true
|
af.index = 1
|
||||||
|
libraryItem.media.audioFiles.push(af)
|
||||||
|
hasUpdated = true
|
||||||
|
} else {
|
||||||
|
this.runSmartTrackOrder(libraryItem, audioScanResult.audioFiles)
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.runSmartTrackOrder(audiobook, audioScanResult.audioFiles)
|
Logger.debug(`[AudioFileScanner] No audio track re-order required`)
|
||||||
|
// Only update metadata not index
|
||||||
|
audioScanResult.audioFiles.forEach((af) => {
|
||||||
|
var existingAF = libraryItem.media.findFileWithInode(af.ino)
|
||||||
|
if (existingAF) {
|
||||||
|
af.index = existingAF.index
|
||||||
|
if (existingAF.updateFromScan && existingAF.updateFromScan(af)) {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set book details from audio file ID3 tags, optional prefer
|
||||||
|
if (libraryItem.media.setMetadataFromAudioFile(preferAudioMetadata)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Logger.debug(`[AudioFileScanner] No audio track re-order required`)
|
|
||||||
// Only update metadata not index
|
|
||||||
audioScanResult.audioFiles.forEach((af) => {
|
|
||||||
var existingAF = audiobook.getAudioFileByIno(af.ino)
|
|
||||||
if (existingAF) {
|
|
||||||
af.index = existingAF.index
|
|
||||||
if (audiobook.updateAudioFile(af)) {
|
|
||||||
hasUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set book details from audio file ID3 tags, optional prefer
|
if (hasUpdated) {
|
||||||
if (audiobook.setDetailsFromFileMetadata(preferAudioMetadata)) {
|
libraryItem.media.rebuildTracks()
|
||||||
hasUpdated = true
|
}
|
||||||
}
|
} // End Book media type
|
||||||
|
|
||||||
if (hasUpdated) {
|
|
||||||
audiobook.rebuildTracks()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return hasUpdated
|
return hasUpdated
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ class LibraryScan {
|
|||||||
this.type = null
|
this.type = null
|
||||||
this.libraryId = null
|
this.libraryId = null
|
||||||
this.libraryName = null
|
this.libraryName = null
|
||||||
|
this.libraryMediaType = null
|
||||||
this.folders = null
|
this.folders = null
|
||||||
this.verbose = false
|
this.verbose = false
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ class LibraryScan {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
libraryName: this.libraryName,
|
libraryName: this.libraryName,
|
||||||
|
libraryMediaType: this.libraryMediaType,
|
||||||
folders: this.folders.map(f => f.toJSON()),
|
folders: this.folders.map(f => f.toJSON()),
|
||||||
scanOptions: this.scanOptions ? this.scanOptions.toJSON() : null,
|
scanOptions: this.scanOptions ? this.scanOptions.toJSON() : null,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
@ -85,6 +87,7 @@ class LibraryScan {
|
|||||||
this.type = type
|
this.type = type
|
||||||
this.libraryId = library.id
|
this.libraryId = library.id
|
||||||
this.libraryName = library.name
|
this.libraryName = library.name
|
||||||
|
this.libraryMediaType = library.mediaType
|
||||||
this.folders = library.folders.map(folder => new Folder(folder.toJSON()))
|
this.folders = library.folders.map(folder => new Folder(folder.toJSON()))
|
||||||
|
|
||||||
this.scanOptions = scanOptions
|
this.scanOptions = scanOptions
|
||||||
|
@ -4,16 +4,20 @@ const Path = require('path')
|
|||||||
// Utils
|
// Utils
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
|
const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder } = require('../utils/scandir')
|
||||||
const { comparePaths, getId } = require('../utils/index')
|
const { comparePaths, getId } = require('../utils/index')
|
||||||
const { ScanResult, LogLevel } = require('../utils/constants')
|
const { ScanResult, LogLevel } = require('../utils/constants')
|
||||||
|
|
||||||
const AudioFileScanner = require('./AudioFileScanner')
|
const AudioFileScanner = require('./AudioFileScanner')
|
||||||
const BookFinder = require('../finders/BookFinder')
|
const BookFinder = require('../finders/BookFinder')
|
||||||
const Audiobook = require('../objects/legacy/Audiobook')
|
const Audiobook = require('../objects/legacy/Audiobook')
|
||||||
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
const LibraryScan = require('./LibraryScan')
|
const LibraryScan = require('./LibraryScan')
|
||||||
const ScanOptions = require('./ScanOptions')
|
const ScanOptions = require('./ScanOptions')
|
||||||
|
|
||||||
|
const Author = require('../objects/entities/Author')
|
||||||
|
const Series = require('../objects/entities/Series')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(db, coverController, emitter) {
|
constructor(db, coverController, emitter) {
|
||||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||||
@ -53,71 +57,69 @@ class Scanner {
|
|||||||
this.cancelLibraryScan[libraryId] = true
|
this.cancelLibraryScan[libraryId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAudiobookById(audiobookId) {
|
async scanLibraryItemById(libraryItemId) {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
|
var libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
||||||
if (!audiobook) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[Scanner] Scan audiobook by id not found ${audiobookId}`)
|
Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
const library = this.db.libraries.find(lib => lib.id === audiobook.libraryId)
|
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Scan audiobook by id library not found "${audiobook.libraryId}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
const folder = library.folders.find(f => f.id === audiobook.folderId)
|
const folder = library.folders.find(f => f.id === libraryItem.folderId)
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id folder not found "${libraryItem.folderId}" in library "${library.name}"`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`)
|
Logger.info(`[Scanner] Scanning Library Item "${libraryItem.media.metadata.title}"`)
|
||||||
return this.scanAudiobook(folder, audiobook)
|
return this.scanLibraryItem(library.mediaType, folder, libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAudiobook(folder, audiobook) {
|
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
|
||||||
var audiobookData = await getAudiobookFileData(folder, audiobook.fullPath, this.db.serverSettings)
|
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, this.db.serverSettings)
|
||||||
if (!audiobookData) {
|
if (!libraryItemData) {
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var checkRes = audiobook.checkScanData(audiobookData, version)
|
var checkRes = libraryItem.checkScanData(libraryItemData)
|
||||||
if (checkRes.updated) hasUpdated = true
|
if (checkRes.updated) hasUpdated = true
|
||||||
|
|
||||||
// Sync other files first so that local images are used as cover art
|
// Sync other files first so that local images are used as cover art
|
||||||
// TODO: Cleanup other file sync
|
if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata)) {
|
||||||
var allOtherFiles = checkRes.newOtherFileData.concat(checkRes.existingOtherFileData)
|
|
||||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.db.serverSettings.scannerPreferOpfMetadata)) {
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan all audio files
|
// Scan all audio files
|
||||||
if (audiobookData.audioFiles.length) {
|
if (libraryItem.hasAudioFiles) {
|
||||||
if (await AudioFileScanner.scanAudioFiles(audiobookData.audioFiles, audiobookData, audiobook, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
var libraryAudioFiles = libraryItem.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
|
if (await AudioFileScanner.scanAudioFiles(libraryAudioFiles, libraryItemData, libraryItem, this.db.serverSettings.scannerPreferAudioMetadata)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract embedded cover art if cover is not already in directory
|
// Extract embedded cover art if cover is not already in directory
|
||||||
if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
|
if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
|
||||||
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
var coverPath = await this.coverController.saveEmbeddedCoverArt(libraryItem)
|
||||||
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
if (coverPath) {
|
||||||
if (relativeDir) {
|
Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
|
||||||
Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('Finished library item scan', libraryItem.hasMediaFiles, hasUpdated)
|
||||||
if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) { // Audiobook is invalid
|
if (!libraryItem.hasMediaFiles) { // Library Item is invalid
|
||||||
audiobook.setInvalid()
|
libraryItem.setInvalid()
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
} else if (audiobook.isInvalid) {
|
} else if (libraryItem.isInvalid) {
|
||||||
audiobook.isInvalid = false
|
libraryItem.isInvalid = false
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
return ScanResult.UPTODATE
|
return ScanResult.UPTODATE
|
||||||
@ -177,241 +179,277 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanLibrary(libraryScan) {
|
async scanLibrary(libraryScan) {
|
||||||
var audiobookDataFound = []
|
var libraryItemDataFound = []
|
||||||
|
|
||||||
// Scan each library
|
// Scan each library
|
||||||
for (let i = 0; i < libraryScan.folders.length; i++) {
|
for (let i = 0; i < libraryScan.folders.length; i++) {
|
||||||
var folder = libraryScan.folders[i]
|
var folder = libraryScan.folders[i]
|
||||||
var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings)
|
var itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder, this.db.serverSettings)
|
||||||
libraryScan.addLog(LogLevel.INFO, `${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`)
|
libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`)
|
||||||
audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder)
|
libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
|
|
||||||
// Remove audiobooks with no inode
|
// Remove audiobooks with no inode
|
||||||
audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
|
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino)
|
||||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId)
|
var libraryItemsInLibrary = this.db.libraryItems.filter(li => li.libraryId === libraryScan.libraryId)
|
||||||
|
|
||||||
const NumScansPerChunk = 25
|
const NumScansPerChunk = 25
|
||||||
const audiobooksToUpdateChunks = []
|
const itemsToUpdateChunks = []
|
||||||
const audiobookDataToRescanChunks = []
|
const itemDataToRescanChunks = []
|
||||||
const newAudiobookDataToScanChunks = []
|
const newItemDataToScanChunks = []
|
||||||
var audiobooksToUpdate = []
|
var itemsToUpdate = []
|
||||||
var audiobookDataToRescan = []
|
var itemDataToRescan = []
|
||||||
var newAudiobookDataToScan = []
|
var newItemDataToScan = []
|
||||||
var audiobooksToFindCovers = []
|
var itemsToFindCovers = []
|
||||||
|
|
||||||
// Check for existing & removed audiobooks
|
// Check for existing & removed library items
|
||||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
for (let i = 0; i < libraryItemsInLibrary.length; i++) {
|
||||||
var audiobook = audiobooksInLibrary[i]
|
var libraryItem = libraryItemsInLibrary[i]
|
||||||
// Find audiobook folder with matching inode or matching path
|
// Find library item folder with matching inode or matching path
|
||||||
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
|
var dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
|
||||||
if (!dataFound) {
|
if (!dataFound) {
|
||||||
libraryScan.addLog(LogLevel.WARN, `Audiobook "${audiobook.title}" is missing`)
|
libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`)
|
||||||
libraryScan.resultsMissing++
|
libraryScan.resultsMissing++
|
||||||
audiobook.setMissing()
|
libraryItem.setMissing()
|
||||||
audiobooksToUpdate.push(audiobook)
|
itemsToUpdate.push(libraryItem)
|
||||||
if (audiobooksToUpdate.length === NumScansPerChunk) {
|
if (itemsToUpdate.length === NumScansPerChunk) {
|
||||||
audiobooksToUpdateChunks.push(audiobooksToUpdate)
|
itemsToUpdateChunks.push(itemsToUpdate)
|
||||||
audiobooksToUpdate = []
|
itemsToUpdate = []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var checkRes = audiobook.checkScanData(dataFound, version)
|
var checkRes = libraryItem.checkScanData(dataFound)
|
||||||
if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length || libraryScan.scanOptions.forceRescan) { // Audiobook has new files
|
if (checkRes.newLibraryFiles.length || libraryScan.scanOptions.forceRescan) { // Item has new files
|
||||||
checkRes.audiobook = audiobook
|
checkRes.libraryItem = libraryItem
|
||||||
checkRes.bookScanData = dataFound
|
checkRes.scanData = dataFound
|
||||||
audiobookDataToRescan.push(checkRes)
|
itemDataToRescan.push(checkRes)
|
||||||
if (audiobookDataToRescan.length === NumScansPerChunk) {
|
if (itemDataToRescan.length === NumScansPerChunk) {
|
||||||
audiobookDataToRescanChunks.push(audiobookDataToRescan)
|
itemDataToRescanChunks.push(itemDataToRescan)
|
||||||
audiobookDataToRescan = []
|
itemDataToRescan = []
|
||||||
}
|
}
|
||||||
} else if (libraryScan.findCovers && audiobook.book.shouldSearchForCover) {
|
} else if (libraryScan.findCovers && libraryItem.media.shouldSearchForCover) {
|
||||||
libraryScan.resultsUpdated++
|
libraryScan.resultsUpdated++
|
||||||
audiobooksToFindCovers.push(audiobook)
|
itemsToFindCovers.push(libraryItem)
|
||||||
audiobooksToUpdate.push(audiobook)
|
itemsToUpdate.push(libraryItem)
|
||||||
if (audiobooksToUpdate.length === NumScansPerChunk) {
|
if (itemsToUpdate.length === NumScansPerChunk) {
|
||||||
audiobooksToUpdateChunks.push(audiobooksToUpdate)
|
itemsToUpdateChunks.push(itemsToUpdate)
|
||||||
audiobooksToUpdate = []
|
itemsToUpdate = []
|
||||||
}
|
}
|
||||||
} else if (checkRes.updated) { // Updated but no scan required
|
} else if (checkRes.updated) { // Updated but no scan required
|
||||||
libraryScan.resultsUpdated++
|
libraryScan.resultsUpdated++
|
||||||
audiobooksToUpdate.push(audiobook)
|
itemsToUpdate.push(libraryItem)
|
||||||
if (audiobooksToUpdate.length === NumScansPerChunk) {
|
if (itemsToUpdate.length === NumScansPerChunk) {
|
||||||
audiobooksToUpdateChunks.push(audiobooksToUpdate)
|
itemsToUpdateChunks.push(itemsToUpdate)
|
||||||
audiobooksToUpdate = []
|
itemsToUpdate = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino)
|
libraryItemDataFound = libraryItemDataFound.filter(lid => lid.ino !== dataFound.ino)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audiobooksToUpdate.length) audiobooksToUpdateChunks.push(audiobooksToUpdate)
|
if (itemsToUpdate.length) itemsToUpdateChunks.push(itemsToUpdate)
|
||||||
if (audiobookDataToRescan.length) audiobookDataToRescanChunks.push(audiobookDataToRescan)
|
if (itemDataToRescan.length) itemDataToRescanChunks.push(itemDataToRescan)
|
||||||
|
|
||||||
// Potential NEW Audiobooks
|
// Potential NEW Library Items
|
||||||
for (let i = 0; i < audiobookDataFound.length; i++) {
|
for (let i = 0; i < libraryItemDataFound.length; i++) {
|
||||||
var dataFound = audiobookDataFound[i]
|
var dataFound = libraryItemDataFound[i]
|
||||||
var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook')
|
|
||||||
if (!hasEbook && !dataFound.audioFiles.length) {
|
var hasMediaFile = dataFound.libraryFiles.some(lf => lf.isMediaFile)
|
||||||
libraryScan.addLog(LogLevel.WARN, `Directory found "${audiobookDataFound.path}" has no ebook or audio files`)
|
if (!hasMediaFile) {
|
||||||
|
libraryScan.addLog(LogLevel.WARN, `Directory found "${libraryItemDataFound.path}" has no media files`)
|
||||||
} else {
|
} else {
|
||||||
newAudiobookDataToScan.push(dataFound)
|
newItemDataToScan.push(dataFound)
|
||||||
if (newAudiobookDataToScan.length === NumScansPerChunk) {
|
if (newItemDataToScan.length === NumScansPerChunk) {
|
||||||
newAudiobookDataToScanChunks.push(newAudiobookDataToScan)
|
newItemDataToScanChunks.push(newItemDataToScan)
|
||||||
newAudiobookDataToScan = []
|
newItemDataToScan = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newAudiobookDataToScan.length) newAudiobookDataToScanChunks.push(newAudiobookDataToScan)
|
if (newItemDataToScan.length) newItemDataToScanChunks.push(newItemDataToScan)
|
||||||
|
|
||||||
// console.log('Num chunks to update', audiobooksToUpdateChunks.length)
|
// Library Items not requiring a scan but require a search for cover
|
||||||
// console.log('Num chunks to rescan', audiobookDataToRescanChunks.length)
|
for (let i = 0; i < itemsToFindCovers.length; i++) {
|
||||||
// console.log('Num chunks to new scan', newAudiobookDataToScanChunks.length)
|
var libraryItem = itemsToFindCovers[i]
|
||||||
|
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
|
||||||
// Audiobooks not requiring a scan but require a search for cover
|
libraryItem.media.updateLastCoverSearch(updatedCover)
|
||||||
for (let i = 0; i < audiobooksToFindCovers.length; i++) {
|
|
||||||
var audiobook = audiobooksToFindCovers[i]
|
|
||||||
var updatedCover = await this.searchForCover(audiobook, libraryScan)
|
|
||||||
audiobook.book.updateLastCoverSearch(updatedCover)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < audiobooksToUpdateChunks.length; i++) {
|
for (let i = 0; i < itemsToUpdateChunks.length; i++) {
|
||||||
await this.updateAudiobooksChunk(audiobooksToUpdateChunks[i])
|
await this.updateLibraryItemChunk(itemsToUpdateChunks[i])
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
// console.log('Update chunk done', i, 'of', audiobooksToUpdateChunks.length)
|
// console.log('Update chunk done', i, 'of', itemsToUpdateChunks.length)
|
||||||
}
|
}
|
||||||
for (let i = 0; i < audiobookDataToRescanChunks.length; i++) {
|
for (let i = 0; i < itemDataToRescanChunks.length; i++) {
|
||||||
await this.rescanAudiobookDataChunk(audiobookDataToRescanChunks[i], libraryScan)
|
await this.rescanLibraryItemDataChunk(itemDataToRescanChunks[i], libraryScan)
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
// console.log('Rescan chunk done', i, 'of', audiobookDataToRescanChunks.length)
|
// console.log('Rescan chunk done', i, 'of', itemDataToRescanChunks.length)
|
||||||
}
|
}
|
||||||
for (let i = 0; i < newAudiobookDataToScanChunks.length; i++) {
|
for (let i = 0; i < newItemDataToScanChunks.length; i++) {
|
||||||
await this.scanNewAudiobookDataChunk(newAudiobookDataToScanChunks[i], libraryScan)
|
await this.scanNewLibraryItemDataChunk(newItemDataToScanChunks[i], libraryScan)
|
||||||
// console.log('New scan chunk done', i, 'of', newAudiobookDataToScanChunks.length)
|
// console.log('New scan chunk done', i, 'of', newItemDataToScanChunks.length)
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAudiobooksChunk(audiobooksToUpdate) {
|
async updateLibraryItemChunk(itemsToUpdate) {
|
||||||
await this.db.updateEntities('audiobook', audiobooksToUpdate)
|
await this.db.updateLibraryItems(itemsToUpdate)
|
||||||
this.emitter('audiobooks_updated', audiobooksToUpdate.map(ab => ab.toJSONExpanded()))
|
this.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async rescanAudiobookDataChunk(audiobookDataToRescan, libraryScan) {
|
async rescanLibraryItemDataChunk(itemDataToRescan, libraryScan) {
|
||||||
var audiobooksUpdated = await Promise.all(audiobookDataToRescan.map((abd) => {
|
var itemsUpdated = await Promise.all(itemDataToRescan.map((lid) => {
|
||||||
return this.rescanAudiobook(abd, libraryScan)
|
return this.rescanLibraryItem(lid, libraryScan)
|
||||||
}))
|
}))
|
||||||
audiobooksUpdated = audiobooksUpdated.filter(ab => ab) // Filter out nulls
|
itemsUpdated = itemsUpdated.filter(li => li) // Filter out nulls
|
||||||
if (audiobooksUpdated.length) {
|
if (itemsUpdated.length) {
|
||||||
libraryScan.resultsUpdated += audiobooksUpdated.length
|
libraryScan.resultsUpdated += itemsUpdated.length
|
||||||
await this.db.updateEntities('audiobook', audiobooksUpdated)
|
await this.db.updateLibraryItems(itemsUpdated)
|
||||||
this.emitter('audiobooks_updated', audiobooksUpdated.map(ab => ab.toJSONExpanded()))
|
this.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanNewAudiobookDataChunk(newAudiobookDataToScan, libraryScan) {
|
async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) {
|
||||||
var newAudiobooks = await Promise.all(newAudiobookDataToScan.map((abd) => {
|
var newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => {
|
||||||
return this.scanNewAudiobook(abd, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan)
|
return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan.preferAudioMetadata, libraryScan.preferOpfMetadata, libraryScan.findCovers, libraryScan)
|
||||||
}))
|
}))
|
||||||
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
|
newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls
|
||||||
libraryScan.resultsAdded += newAudiobooks.length
|
libraryScan.resultsAdded += newLibraryItems.length
|
||||||
await this.db.insertAudiobooks(newAudiobooks)
|
await this.db.insertLibraryItems(newLibraryItems)
|
||||||
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
|
this.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async rescanAudiobook(audiobookCheckData, libraryScan) {
|
async rescanLibraryItem(libraryItemCheckData, libraryScan) {
|
||||||
const { newAudioFileData, audioFilesRemoved, newOtherFileData, audiobook, bookScanData, updated, existingAudioFileData, existingOtherFileData } = audiobookCheckData
|
const { newLibraryFiles, filesRemoved, existingLibraryFiles, libraryItem, scanData, updated } = libraryItemCheckData
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Library "${libraryScan.libraryName}" Re-scanning "${audiobook.path}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library "${libraryScan.libraryName}" Re-scanning "${libraryItem.path}"`)
|
||||||
var hasUpdated = updated
|
var hasUpdated = updated
|
||||||
|
|
||||||
// Sync other files first to use local images as cover before extracting audio file cover
|
// Sync other files first to use local images as cover before extracting audio file cover
|
||||||
if (newOtherFileData.length || libraryScan.scanOptions.forceRescan) {
|
if (await libraryItem.syncFiles(libraryScan.preferOpfMetadata)) {
|
||||||
// TODO: Cleanup other file sync
|
hasUpdated = true
|
||||||
var allOtherFiles = newOtherFileData.concat(existingOtherFileData)
|
|
||||||
if (await audiobook.syncOtherFiles(allOtherFiles, libraryScan.preferOpfMetadata)) {
|
|
||||||
hasUpdated = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
// forceRescan all existing audio files - will probe and update ID3 tag metadata
|
||||||
if (libraryScan.scanOptions.forceRescan && existingAudioFileData.length) {
|
var existingAudioFiles = existingLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (await AudioFileScanner.scanAudioFiles(existingAudioFileData, bookScanData, audiobook, libraryScan.preferAudioMetadata, libraryScan)) {
|
if (libraryScan.scanOptions.forceRescan && existingAudioFiles.length) {
|
||||||
|
if (await AudioFileScanner.scanAudioFiles(existingAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scan new audio files
|
// Scan new audio files
|
||||||
if (newAudioFileData.length || audioFilesRemoved.length) {
|
var newAudioFiles = newLibraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
if (await AudioFileScanner.scanAudioFiles(newAudioFileData, bookScanData, audiobook, libraryScan.preferAudioMetadata, libraryScan)) {
|
var removedAudioFiles = filesRemoved.filter(lf => lf.fileType === 'audio')
|
||||||
|
if (newAudioFiles.length || removedAudioFiles.length) {
|
||||||
|
if (await AudioFileScanner.scanAudioFiles(newAudioFiles, scanData, libraryItem, libraryScan.preferAudioMetadata, libraryScan)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If an audio file has embedded cover art and no cover is set yet, extract & use it
|
// If an audio file has embedded cover art and no cover is set yet, extract & use it
|
||||||
if (newAudioFileData.length || libraryScan.scanOptions.forceRescan) {
|
if (newAudioFiles.length || libraryScan.scanOptions.forceRescan) {
|
||||||
if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
|
if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
|
||||||
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
var savedCoverPath = await this.coverController.saveEmbeddedCoverArt(libraryItem)
|
||||||
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
if (savedCoverPath) {
|
||||||
if (relativeDir) {
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${relativeDir}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${savedCoverPath}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) { // Audiobook is invalid
|
if (!libraryItem.media.hasMediaFiles) { // Library item is invalid
|
||||||
audiobook.setInvalid()
|
libraryItem.setInvalid()
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
} else if (audiobook.isInvalid) {
|
} else if (libraryItem.isInvalid) {
|
||||||
audiobook.isInvalid = false
|
libraryItem.isInvalid = false
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for cover if enabled and has no cover (and author or title has changed OR has been 7 days since last lookup)
|
// Scan for cover if enabled and has no cover (and author or title has changed OR has been 7 days since last lookup)
|
||||||
if (audiobook && libraryScan.findCovers && !audiobook.cover && audiobook.book.shouldSearchForCover) {
|
if (libraryScan.findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
|
||||||
var updatedCover = await this.searchForCover(audiobook, libraryScan)
|
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
|
||||||
audiobook.book.updateLastCoverSearch(updatedCover)
|
libraryItem.media.updateLastCoverSearch(updatedCover)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasUpdated ? audiobook : null
|
return hasUpdated ? libraryItem : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanNewAudiobook(audiobookData, preferAudioMetadata, preferOpfMetadata, findCovers, libraryScan = null) {
|
async scanNewLibraryItem(libraryItemData, libraryMediaType, preferAudioMetadata, preferOpfMetadata, findCovers, libraryScan = null) {
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new book "${audiobookData.path}"`)
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`)
|
||||||
else Logger.debug(`[Scanner] Scanning new book "${audiobookData.path}"`)
|
else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`)
|
||||||
|
|
||||||
var audiobook = new Audiobook()
|
var libraryItem = new LibraryItem()
|
||||||
audiobook.setData(audiobookData)
|
libraryItem.setData(libraryMediaType, libraryItemData)
|
||||||
|
|
||||||
if (audiobookData.audioFiles.length) {
|
var audioFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio')
|
||||||
await AudioFileScanner.scanAudioFiles(audiobookData.audioFiles, audiobookData, audiobook, preferAudioMetadata, libraryScan)
|
if (audioFiles.length) {
|
||||||
|
await AudioFileScanner.scanAudioFiles(audioFiles, libraryItemData, libraryItem, preferAudioMetadata, libraryScan)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audiobook.audioFilesToInclude.length && !audiobook.ebooks.length) {
|
if (!libraryItem.media.hasMediaFiles) {
|
||||||
// Audiobook has no ebooks and no valid audio tracks do not continue
|
Logger.warn(`[Scanner] Library item has no media files "${libraryItemData.path}"`)
|
||||||
Logger.warn(`[Scanner] Audiobook has no ebooks and no valid audio tracks "${audiobook.path}"`)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for desc.txt and reader.txt and update
|
await libraryItem.syncFiles(preferOpfMetadata)
|
||||||
await audiobook.saveDataFromTextFiles(preferOpfMetadata)
|
|
||||||
|
|
||||||
// Extract embedded cover art if cover is not already in directory
|
// Extract embedded cover art if cover is not already in directory
|
||||||
if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
|
if (libraryItem.media.hasEmbeddedCoverArt && !libraryItem.media.coverPath) {
|
||||||
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
var coverPath = await this.coverController.saveEmbeddedCoverArt(libraryItem)
|
||||||
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
if (coverPath) {
|
||||||
if (relativeDir) {
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${coverPath}"`)
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Saved embedded cover art "${relativeDir}"`)
|
else Logger.debug(`[Scanner] Saved embedded cover art "${coverPath}"`)
|
||||||
else Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for cover if enabled and has no cover
|
// Scan for cover if enabled and has no cover
|
||||||
if (audiobook && findCovers && !audiobook.cover && audiobook.book.shouldSearchForCover) {
|
if (libraryMediaType !== 'podcast') {
|
||||||
var updatedCover = await this.searchForCover(audiobook, libraryScan)
|
if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) {
|
||||||
audiobook.book.updateLastCoverSearch(updatedCover)
|
var updatedCover = await this.searchForCover(libraryItem, libraryScan)
|
||||||
|
libraryItem.media.updateLastCoverSearch(updatedCover)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or match all new authors and series
|
||||||
|
if (libraryItem.media.metadata.authors.some(au => au.id.startsWith('new'))) {
|
||||||
|
var newAuthors = []
|
||||||
|
libraryItem.media.metadata.authors = libraryItem.media.metadata.authors.map((tempMinAuthor) => {
|
||||||
|
var _author = this.db.authors.find(au => au.checkNameEquals(tempMinAuthor.name))
|
||||||
|
if (!_author) {
|
||||||
|
_author = new Author()
|
||||||
|
_author.setData(tempMinAuthor)
|
||||||
|
newAuthors.push(_author)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: _author.id,
|
||||||
|
name: _author.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (newAuthors.length) {
|
||||||
|
await this.db.insertEntities('author', newAuthors)
|
||||||
|
this.emitter('authors_added', newAuthors.map(au => au.toJSON()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) {
|
||||||
|
var newSeries = []
|
||||||
|
libraryItem.media.metadata.series = libraryItem.media.metadata.series.map((tempMinSeries) => {
|
||||||
|
var _series = this.db.series.find(se => se.checkNameEquals(tempMinSeries.name))
|
||||||
|
if (!_series) {
|
||||||
|
_series = new Series()
|
||||||
|
_series.setData(tempMinSeries)
|
||||||
|
newSeries.push(_series)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: _series.id,
|
||||||
|
name: _series.name,
|
||||||
|
sequence: tempMinSeries.sequence
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (newSeries.length) {
|
||||||
|
await this.db.insertEntities('series', newSeries)
|
||||||
|
this.emitter('series_added', newSeries.map(se => se.toJSON()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return audiobook
|
return libraryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileUpdatesGrouped(fileUpdates) {
|
getFileUpdatesGrouped(fileUpdates) {
|
||||||
@ -448,113 +486,113 @@ class Scanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
|
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
|
||||||
var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true)
|
var fileUpdateGroup = groupFilesIntoLibraryItemPaths(relFilePaths, true)
|
||||||
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateBookGroup)
|
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
|
||||||
Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
|
Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanFolderUpdates(library, folder, fileUpdateBookGroup) {
|
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
||||||
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
||||||
|
|
||||||
// First pass - Remove files in parent dirs of audiobooks and remap the fileupdate group
|
// First pass - Remove files in parent dirs of items and remap the fileupdate group
|
||||||
// Test Case: Moving audio files from audiobook folder to author folder should trigger a re-scan of audiobook
|
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
|
||||||
var updateGroup = { ...fileUpdateBookGroup }
|
var updateGroup = { ...fileUpdateGroup }
|
||||||
for (const bookDir in updateGroup) {
|
for (const itemDir in updateGroup) {
|
||||||
var bookDirNestedFiles = fileUpdateBookGroup[bookDir].filter(b => b.includes('/'))
|
var itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/'))
|
||||||
if (!bookDirNestedFiles.length) continue;
|
if (!itemDirNestedFiles.length) continue;
|
||||||
|
|
||||||
var firstNest = bookDirNestedFiles[0].split('/').shift()
|
var firstNest = itemDirNestedFiles[0].split('/').shift()
|
||||||
var altDir = `${bookDir}/${firstNest}`
|
var altDir = `${itemDir}/${firstNest}`
|
||||||
|
|
||||||
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), bookDir)
|
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), itemDir)
|
||||||
var childAudiobook = this.db.audiobooks.find(ab => ab.fullPath !== fullPath && ab.fullPath.startsWith(fullPath))
|
var childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.fullPath.startsWith(fullPath))
|
||||||
if (!childAudiobook) {
|
if (!childLibraryItem) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var altFullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), altDir)
|
var altFullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), altDir)
|
||||||
var altChildAudiobook = this.db.audiobooks.find(ab => ab.fullPath !== altFullPath && ab.fullPath.startsWith(altFullPath))
|
var altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath))
|
||||||
if (altChildAudiobook) {
|
if (altChildLibraryItem) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete fileUpdateBookGroup[bookDir]
|
delete fileUpdateGroup[itemDir]
|
||||||
fileUpdateBookGroup[altDir] = bookDirNestedFiles.map((f) => f.split('/').slice(1).join('/'))
|
fileUpdateGroup[altDir] = itemDirNestedFiles.map((f) => f.split('/').slice(1).join('/'))
|
||||||
Logger.warn(`[Scanner] Some files were modified in a parent directory of an audiobook "${childAudiobook.title}" - ignoring`)
|
Logger.warn(`[Scanner] Some files were modified in a parent directory of a library item "${childLibraryItem.title}" - ignoring`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: Check for new/updated/removed audiobooks
|
// Second pass: Check for new/updated/removed items
|
||||||
var bookGroupingResults = {}
|
var itemGroupingResults = {}
|
||||||
for (const bookDir in fileUpdateBookGroup) {
|
for (const itemDir in fileUpdateGroup) {
|
||||||
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), bookDir)
|
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), itemDir)
|
||||||
|
|
||||||
// Check if book dir group is already an audiobook
|
// Check if book dir group is already an item
|
||||||
var existingAudiobook = this.db.audiobooks.find(ab => fullPath.startsWith(ab.fullPath))
|
var existingLibraryItem = this.db.libraryItems.find(li => fullPath.startsWith(li.path))
|
||||||
if (existingAudiobook) {
|
if (existingLibraryItem) {
|
||||||
|
|
||||||
// Is the audiobook exactly - check if was deleted
|
// Is the item exactly - check if was deleted
|
||||||
if (existingAudiobook.fullPath === fullPath) {
|
if (existingLibraryItem.path === fullPath) {
|
||||||
var exists = await fs.pathExists(fullPath)
|
var exists = await fs.pathExists(fullPath)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
Logger.info(`[Scanner] Scanning file update group and audiobook was deleted "${existingAudiobook.title}" - marking as missing`)
|
Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`)
|
||||||
existingAudiobook.setMissing()
|
existingLibraryItem.setMissing()
|
||||||
await this.db.updateAudiobook(existingAudiobook)
|
await this.db.updateLibraryItem(existingLibraryItem)
|
||||||
this.emitter('audiobook_updated', existingAudiobook.toJSONExpanded())
|
this.emitter('item_updated', existingLibraryItem.toJSONExpanded())
|
||||||
|
|
||||||
bookGroupingResults[bookDir] = ScanResult.REMOVED
|
itemGroupingResults[itemDir] = ScanResult.REMOVED
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan audiobook for updates
|
// Scan library item for updates
|
||||||
Logger.debug(`[Scanner] Folder update for relative path "${bookDir}" is in audiobook "${existingAudiobook.title}" - scan for updates`)
|
Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`)
|
||||||
bookGroupingResults[bookDir] = await this.scanAudiobook(folder, existingAudiobook)
|
itemGroupingResults[itemDir] = await this.scanLibraryItem(library.mediaType, folder, existingLibraryItem)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if an audiobook is a subdirectory of this dir
|
// Check if a library item is a subdirectory of this dir
|
||||||
var childAudiobook = this.db.audiobooks.find(ab => ab.fullPath.startsWith(fullPath))
|
var childItem = this.db.libraryItems.find(li => li.path.startsWith(fullPath))
|
||||||
if (childAudiobook) {
|
if (childItem) {
|
||||||
Logger.warn(`[Scanner] Files were modified in a parent directory of an audiobook "${childAudiobook.title}" - ignoring`)
|
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
||||||
bookGroupingResults[bookDir] = ScanResult.NOTHING
|
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
|
Logger.debug(`[Scanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`)
|
||||||
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
|
var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath)
|
||||||
if (newAudiobook) {
|
if (newLibraryItem) {
|
||||||
await this.db.insertAudiobook(newAudiobook)
|
await this.db.insertLibraryItem(newLibraryItem)
|
||||||
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
|
this.emitter('item_added', newLibraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
|
itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
|
|
||||||
return bookGroupingResults
|
return itemGroupingResults
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanPotentialNewAudiobook(folder, fullPath) {
|
async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath) {
|
||||||
var audiobookData = await getAudiobookFileData(folder, fullPath, this.db.serverSettings)
|
var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, this.db.serverSettings)
|
||||||
if (!audiobookData) return null
|
if (!libraryItemData) return null
|
||||||
var serverSettings = this.db.serverSettings
|
var serverSettings = this.db.serverSettings
|
||||||
return this.scanNewAudiobook(audiobookData, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)
|
return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers)
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchForCover(audiobook, libraryScan = null) {
|
async searchForCover(libraryItem, libraryScan = null) {
|
||||||
var options = {
|
var options = {
|
||||||
titleDistance: 2,
|
titleDistance: 2,
|
||||||
authorDistance: 2
|
authorDistance: 2
|
||||||
}
|
}
|
||||||
var scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
|
var scannerCoverProvider = this.db.serverSettings.scannerCoverProvider
|
||||||
var results = await this.bookFinder.findCovers(scannerCoverProvider, audiobook.title, audiobook.authorFL, options)
|
var results = await this.bookFinder.findCovers(scannerCoverProvider, libraryItem.media.metadata.title, libraryItem.media.metadata.authorName, options)
|
||||||
if (results.length) {
|
if (results.length) {
|
||||||
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${audiobook.title}"`)
|
if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Found best cover for "${libraryItem.media.metadata.title}"`)
|
||||||
else Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
|
else Logger.debug(`[Scanner] Found best cover for "${libraryItem.media.metadata.title}"`)
|
||||||
|
|
||||||
// If the first cover result fails, attempt to download the second
|
// If the first cover result fails, attempt to download the second
|
||||||
for (let i = 0; i < results.length && i < 2; i++) {
|
for (let i = 0; i < results.length && i < 2; i++) {
|
||||||
|
|
||||||
// Downloads and updates the book cover
|
// Downloads and updates the book cover
|
||||||
var result = await this.coverController.downloadCoverFromUrl(audiobook, results[i])
|
var result = await this.coverController.downloadCoverFromUrl(libraryItem, results[i])
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
|
Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
|
||||||
|
@ -8,8 +8,6 @@ const bookKeyMap = {
|
|||||||
subtitle: 'subtitle',
|
subtitle: 'subtitle',
|
||||||
author: 'authorFL',
|
author: 'authorFL',
|
||||||
narrator: 'narratorFL',
|
narrator: 'narratorFL',
|
||||||
series: 'series',
|
|
||||||
volumeNumber: 'volumeNumber',
|
|
||||||
publishYear: 'publishYear',
|
publishYear: 'publishYear',
|
||||||
publisher: 'publisher',
|
publisher: 'publisher',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
@ -39,7 +37,7 @@ function generate(audiobook, outputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fs.writeFile(outputPath, fileString).then(() => {
|
return fs.writeFile(outputPath, fileString).then(() => {
|
||||||
return filePerms(outputPath, 0o774, global.Uid, global.Gid, true).then((data) => true)
|
return filePerms.setDefault(outputPath, true).then(() => true)
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
||||||
return false
|
return false
|
||||||
|
@ -77,7 +77,19 @@ const chmodr = (p, mode, uid, gid, cb) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (path, mode, uid, gid, silent = false) => {
|
// Set custom permissions
|
||||||
|
module.exports.set = (path, mode, uid, gid, silent = false) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
|
chmodr(path, mode, uid, gid, resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default permissions 0o744 and global Uid/Gid
|
||||||
|
module.exports.setDefault = (path, silent = false) => {
|
||||||
|
const mode = 0o744
|
||||||
|
const uid = global.Uid
|
||||||
|
const gid = global.Gid
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
chmodr(path, mode, uid, gid, resolve)
|
chmodr(path, mode, uid, gid, resolve)
|
||||||
|
@ -3,7 +3,7 @@ const globals = {
|
|||||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'],
|
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'],
|
||||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
TextFileTypes: ['txt', 'nfo'],
|
TextFileTypes: ['txt', 'nfo'],
|
||||||
MetadataFileTypes: ['opf', 'abs']
|
MetadataFileTypes: ['opf', 'abs', 'xml']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = globals
|
module.exports = globals
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
const parseFullName = require('./parseFullName')
|
|
||||||
|
|
||||||
function parseName(name) {
|
|
||||||
var parts = parseFullName(name)
|
|
||||||
var firstName = parts.first
|
|
||||||
if (firstName && parts.middle) firstName += ' ' + parts.middle
|
|
||||||
|
|
||||||
return {
|
|
||||||
first_name: firstName,
|
|
||||||
last_name: parts.last
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this name segment is of the format "Last, First" or "First Last"
|
|
||||||
// return true is "Last, First"
|
|
||||||
function checkIsALastName(name) {
|
|
||||||
if (!name.includes(' ')) return true // No spaces must be a Last name
|
|
||||||
|
|
||||||
var parsed = parseFullName(name)
|
|
||||||
if (!parsed.first) return true // had spaces but not a first name i.e. "von Mises", must be last name only
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (author) => {
|
|
||||||
if (!author) return null
|
|
||||||
|
|
||||||
var splitAuthors = []
|
|
||||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
|
||||||
if (author.includes('&')) {
|
|
||||||
author.split('&').forEach((asa) => splitAuthors = splitAuthors.concat(asa.split(',')))
|
|
||||||
} else {
|
|
||||||
splitAuthors = author.split(',')
|
|
||||||
}
|
|
||||||
if (splitAuthors.length) splitAuthors = splitAuthors.map(a => a.trim())
|
|
||||||
|
|
||||||
var authors = []
|
|
||||||
|
|
||||||
// 1 author FIRST LAST
|
|
||||||
if (splitAuthors.length === 1) {
|
|
||||||
authors.push(parseName(author))
|
|
||||||
} else {
|
|
||||||
var firstChunkIsALastName = checkIsALastName(splitAuthors[0])
|
|
||||||
var isEvenNum = splitAuthors.length % 2 === 0
|
|
||||||
|
|
||||||
if (!isEvenNum && firstChunkIsALastName) {
|
|
||||||
// console.error('Multi-author LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it')
|
|
||||||
splitAuthors = splitAuthors.slice(0, splitAuthors.length - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChunkIsALastName) {
|
|
||||||
var numAuthors = splitAuthors.length / 2
|
|
||||||
for (let i = 0; i < numAuthors; i++) {
|
|
||||||
var last = splitAuthors.shift()
|
|
||||||
var first = splitAuthors.shift()
|
|
||||||
authors.push({
|
|
||||||
first_name: first,
|
|
||||||
last_name: last
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
splitAuthors.forEach((segment) => {
|
|
||||||
authors.push(parseName(segment))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstLast = authors.length ? authors.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name).join(', ') : ''
|
|
||||||
var lastFirst = authors.length ? authors.map(a => a.first_name ? `${a.last_name}, ${a.first_name}` : a.last_name).join(', ') : ''
|
|
||||||
return {
|
|
||||||
authorFL: firstLast,
|
|
||||||
authorLF: lastFirst,
|
|
||||||
authorsParsed: authors
|
|
||||||
}
|
|
||||||
}
|
|
82
server/utils/parseNameString.js
Normal file
82
server/utils/parseNameString.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// This takes a string and parsed out first and last names
|
||||||
|
// accepts comma separated lists e.g. "Jon Smith, Jane Smith" or "Smith, Jon, Smith, Jane"
|
||||||
|
// can be separated by "&" e.g. "Jon Smith & Jane Smith" or "Smith, Jon & Smith, Jane"
|
||||||
|
//
|
||||||
|
const parseFullName = require('./parseFullName')
|
||||||
|
|
||||||
|
function parseName(name) {
|
||||||
|
var parts = parseFullName(name)
|
||||||
|
var firstName = parts.first
|
||||||
|
if (firstName && parts.middle) firstName += ' ' + parts.middle
|
||||||
|
|
||||||
|
return {
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: parts.last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this name segment is of the format "Last, First" or "First Last"
|
||||||
|
// return true is "Last, First"
|
||||||
|
function checkIsALastName(name) {
|
||||||
|
if (!name.includes(' ')) return true // No spaces must be a Last name
|
||||||
|
|
||||||
|
var parsed = parseFullName(name)
|
||||||
|
if (!parsed.first) return true // had spaces but not a first name i.e. "von Mises", must be last name only
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (nameString) => {
|
||||||
|
if (!nameString) return null
|
||||||
|
|
||||||
|
var splitNames = []
|
||||||
|
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||||
|
if (nameString.includes('&')) {
|
||||||
|
nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||||
|
} else {
|
||||||
|
splitNames = nameString.split(',')
|
||||||
|
}
|
||||||
|
if (splitNames.length) splitNames = splitNames.map(a => a.trim())
|
||||||
|
|
||||||
|
var names = []
|
||||||
|
|
||||||
|
// 1 name FIRST LAST
|
||||||
|
if (splitNames.length === 1) {
|
||||||
|
names.push(parseName(nameString))
|
||||||
|
} else {
|
||||||
|
var firstChunkIsALastName = checkIsALastName(splitNames[0])
|
||||||
|
var isEvenNum = splitNames.length % 2 === 0
|
||||||
|
|
||||||
|
if (!isEvenNum && firstChunkIsALastName) {
|
||||||
|
// console.error('Multi-name LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it')
|
||||||
|
splitNames = splitNames.slice(0, splitNames.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChunkIsALastName) {
|
||||||
|
var num = splitNames.length / 2
|
||||||
|
for (let i = 0; i < num; i++) {
|
||||||
|
var last = splitNames.shift()
|
||||||
|
var first = splitNames.shift()
|
||||||
|
names.push({
|
||||||
|
first_name: first,
|
||||||
|
last_name: last
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
splitNames.forEach((segment) => {
|
||||||
|
names.push(parseName(segment))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var namesArray = names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)
|
||||||
|
var firstLast = names.length ? namesArray.join(', ') : ''
|
||||||
|
var lastFirst = names.length ? names.map(a => a.first_name ? `${a.last_name}, ${a.first_name}` : a.last_name).join(', ') : ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
nameFL: firstLast, // String of comma separated first last
|
||||||
|
nameLF: lastFirst, // String of comma separated last, first
|
||||||
|
names: namesArray // Array of first last
|
||||||
|
}
|
||||||
|
}
|
@ -71,20 +71,20 @@ function fetchLanguage(metadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchSeries(metadata) {
|
function fetchSeries(metadata) {
|
||||||
if(typeof metadata.meta == "undefined") return null
|
if (typeof metadata.meta == "undefined") return null
|
||||||
return fetchTagString(metadata.meta, "calibre:series")
|
return fetchTagString(metadata.meta, "calibre:series")
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchVolumeNumber(metadata) {
|
function fetchVolumeNumber(metadata) {
|
||||||
if(typeof metadata.meta == "undefined") return null
|
if (typeof metadata.meta == "undefined") return null
|
||||||
return fetchTagString(metadata.meta, "calibre:series_index")
|
return fetchTagString(metadata.meta, "calibre:series_index")
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchNarrators(creators, metadata) {
|
function fetchNarrators(creators, metadata) {
|
||||||
var roleNrt = fetchCreator(creators, 'nrt')
|
var roleNrt = fetchCreator(creators, 'nrt')
|
||||||
if(typeof metadata.meta == "undefined" || roleNrt != null) return roleNrt
|
if (typeof metadata.meta == "undefined" || roleNrt != null) return roleNrt
|
||||||
try {
|
try {
|
||||||
var narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/"/g,'"'))
|
var narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/"/g, '"'))
|
||||||
return narratorsJSON["#value#"].join(", ")
|
return narratorsJSON["#value#"].join(", ")
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@ -103,7 +103,7 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
|||||||
|
|
||||||
if (typeof metadata.meta != "undefined") {
|
if (typeof metadata.meta != "undefined") {
|
||||||
metadata.meta = {}
|
metadata.meta = {}
|
||||||
for(var match of xml.matchAll(/<meta name="(?<name>.+)" content="(?<content>.+)"\/>/g)) {
|
for (var match of xml.matchAll(/<meta name="(?<name>.+)" content="(?<content>.+)"\/>/g)) {
|
||||||
metadata.meta[match.groups['name']] = [match.groups['content']]
|
metadata.meta[match.groups['name']] = [match.groups['content']]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
|||||||
genres: fetchGenres(metadata),
|
genres: fetchGenres(metadata),
|
||||||
language: fetchLanguage(metadata),
|
language: fetchLanguage(metadata),
|
||||||
series: fetchSeries(metadata),
|
series: fetchSeries(metadata),
|
||||||
volumeNumber: fetchVolumeNumber(metadata)
|
sequence: fetchVolumeNumber(metadata)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
}
|
@ -3,8 +3,9 @@ const fs = require('fs-extra')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
|
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
|
||||||
const globals = require('./globals')
|
const globals = require('./globals')
|
||||||
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
|
|
||||||
function isBookFile(path) {
|
function isMediaFile(path) {
|
||||||
if (!path) return false
|
if (!path) return false
|
||||||
var ext = Path.extname(path)
|
var ext = Path.extname(path)
|
||||||
if (!ext) return false
|
if (!ext) return false
|
||||||
@ -14,8 +15,8 @@ function isBookFile(path) {
|
|||||||
|
|
||||||
// TODO: Function needs to be re-done
|
// TODO: Function needs to be re-done
|
||||||
// Input: array of relative file paths
|
// Input: array of relative file paths
|
||||||
// Output: map of files grouped into potential audiobook dirs
|
// Output: map of files grouped into potential item dirs
|
||||||
function groupFilesIntoAudiobookPaths(paths) {
|
function groupFilesIntoLibraryItemPaths(paths) {
|
||||||
// Step 1: Clean path, Remove leading "/", Filter out files in root dir
|
// Step 1: Clean path, Remove leading "/", Filter out files in root dir
|
||||||
var pathsFiltered = paths.map(path => {
|
var pathsFiltered = paths.map(path => {
|
||||||
return path.startsWith('/') ? path.slice(1) : path
|
return path.startsWith('/') ? path.slice(1) : path
|
||||||
@ -29,7 +30,7 @@ function groupFilesIntoAudiobookPaths(paths) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Step 3: Group files in dirs
|
// Step 3: Group files in dirs
|
||||||
var audiobookGroup = {}
|
var itemGroup = {}
|
||||||
pathsFiltered.forEach((path) => {
|
pathsFiltered.forEach((path) => {
|
||||||
var dirparts = Path.dirname(path).split('/')
|
var dirparts = Path.dirname(path).split('/')
|
||||||
var numparts = dirparts.length
|
var numparts = dirparts.length
|
||||||
@ -40,41 +41,41 @@ function groupFilesIntoAudiobookPaths(paths) {
|
|||||||
var dirpart = dirparts.shift()
|
var dirpart = dirparts.shift()
|
||||||
_path = Path.posix.join(_path, dirpart)
|
_path = Path.posix.join(_path, dirpart)
|
||||||
|
|
||||||
if (audiobookGroup[_path]) { // Directory already has files, add file
|
if (itemGroup[_path]) { // Directory already has files, add file
|
||||||
var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path))
|
var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path))
|
||||||
audiobookGroup[_path].push(relpath)
|
itemGroup[_path].push(relpath)
|
||||||
return
|
return
|
||||||
} else if (!dirparts.length) { // This is the last directory, create group
|
} else if (!dirparts.length) { // This is the last directory, create group
|
||||||
audiobookGroup[_path] = [Path.basename(path)]
|
itemGroup[_path] = [Path.basename(path)]
|
||||||
return
|
return
|
||||||
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
|
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
|
||||||
audiobookGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))]
|
itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return audiobookGroup
|
return itemGroup
|
||||||
}
|
}
|
||||||
module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths
|
module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
|
||||||
|
|
||||||
// Input: array of relative file items (see recurseFiles)
|
// Input: array of relative file items (see recurseFiles)
|
||||||
// Output: map of files grouped into potential audiobook dirs
|
// Output: map of files grouped into potential libarary item dirs
|
||||||
function groupFileItemsIntoBooks(fileItems) {
|
function groupFileItemsIntoLibraryItemDirs(fileItems) {
|
||||||
// Step 1: Filter out files in root dir (with depth of 0)
|
// Step 1: Filter out files in root dir (with depth of 0)
|
||||||
var itemsFiltered = fileItems.filter(i => i.deep > 0)
|
var itemsFiltered = fileItems.filter(i => i.deep > 0)
|
||||||
|
|
||||||
// Step 2: Seperate audio/ebook files and other files
|
// Step 2: Seperate media files and other files
|
||||||
// - Directories without an audio or ebook file will not be included
|
// - Directories without a media file will not be included
|
||||||
var bookFileItems = []
|
var mediaFileItems = []
|
||||||
var otherFileItems = []
|
var otherFileItems = []
|
||||||
itemsFiltered.forEach(item => {
|
itemsFiltered.forEach(item => {
|
||||||
if (isBookFile(item.fullpath)) bookFileItems.push(item)
|
if (isMediaFile(item.fullpath)) mediaFileItems.push(item)
|
||||||
else otherFileItems.push(item)
|
else otherFileItems.push(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 3: Group audio files in audiobooks
|
// Step 3: Group audio files in library items
|
||||||
var audiobookGroup = {}
|
var libraryItemGroup = {}
|
||||||
bookFileItems.forEach((item) => {
|
mediaFileItems.forEach((item) => {
|
||||||
var dirparts = item.reldirpath.split('/')
|
var dirparts = item.reldirpath.split('/')
|
||||||
var numparts = dirparts.length
|
var numparts = dirparts.length
|
||||||
var _path = ''
|
var _path = ''
|
||||||
@ -84,21 +85,21 @@ function groupFileItemsIntoBooks(fileItems) {
|
|||||||
var dirpart = dirparts.shift()
|
var dirpart = dirparts.shift()
|
||||||
_path = Path.posix.join(_path, dirpart)
|
_path = Path.posix.join(_path, dirpart)
|
||||||
|
|
||||||
if (audiobookGroup[_path]) { // Directory already has files, add file
|
if (libraryItemGroup[_path]) { // Directory already has files, add file
|
||||||
var relpath = Path.posix.join(dirparts.join('/'), item.name)
|
var relpath = Path.posix.join(dirparts.join('/'), item.name)
|
||||||
audiobookGroup[_path].push(relpath)
|
libraryItemGroup[_path].push(relpath)
|
||||||
return
|
return
|
||||||
} else if (!dirparts.length) { // This is the last directory, create group
|
} else if (!dirparts.length) { // This is the last directory, create group
|
||||||
audiobookGroup[_path] = [item.name]
|
libraryItemGroup[_path] = [item.name]
|
||||||
return
|
return
|
||||||
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
|
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group
|
||||||
audiobookGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
|
libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 4: Add other files into audiobook groups
|
// Step 4: Add other files into library item groups
|
||||||
otherFileItems.forEach((item) => {
|
otherFileItems.forEach((item) => {
|
||||||
var dirparts = item.reldirpath.split('/')
|
var dirparts = item.reldirpath.split('/')
|
||||||
var numparts = dirparts.length
|
var numparts = dirparts.length
|
||||||
@ -108,30 +109,23 @@ function groupFileItemsIntoBooks(fileItems) {
|
|||||||
for (let i = 0; i < numparts; i++) {
|
for (let i = 0; i < numparts; i++) {
|
||||||
var dirpart = dirparts.shift()
|
var dirpart = dirparts.shift()
|
||||||
_path = Path.posix.join(_path, dirpart)
|
_path = Path.posix.join(_path, dirpart)
|
||||||
if (audiobookGroup[_path]) { // Directory is audiobook group
|
if (libraryItemGroup[_path]) { // Directory is audiobook group
|
||||||
var relpath = Path.posix.join(dirparts.join('/'), item.name)
|
var relpath = Path.posix.join(dirparts.join('/'), item.name)
|
||||||
audiobookGroup[_path].push(relpath)
|
libraryItemGroup[_path].push(relpath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return audiobookGroup
|
return libraryItemGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanFileObjects(basepath, abrelpath, files) {
|
function cleanFileObjects(libraryItemPath, libraryItemRelPath, files) {
|
||||||
return Promise.all(files.map(async (file) => {
|
return Promise.all(files.map(async (file) => {
|
||||||
var fullPath = Path.posix.join(basepath, file)
|
var filePath = Path.posix.join(libraryItemPath, file)
|
||||||
var fileTsData = await getFileTimestampsWithIno(fullPath)
|
var relFilePath = Path.posix.join(libraryItemRelPath, file)
|
||||||
|
var newLibraryFile = new LibraryFile()
|
||||||
var ext = Path.extname(file)
|
await newLibraryFile.setDataFromPath(filePath, relFilePath)
|
||||||
return {
|
return newLibraryFile
|
||||||
filetype: getFileType(ext),
|
|
||||||
filename: Path.basename(file),
|
|
||||||
path: Path.posix.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3
|
|
||||||
fullPath, // /audiobooks/AUDIOBOOK/PATH/filename.mp3
|
|
||||||
ext: ext,
|
|
||||||
...fileTsData
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,9 +142,8 @@ function getFileType(ext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan folder
|
// Scan folder
|
||||||
async function scanRootDir(folder, serverSettings = {}) {
|
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
||||||
var folderPath = folder.fullPath.replace(/\\/g, '/')
|
var folderPath = folder.fullPath.replace(/\\/g, '/')
|
||||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
|
||||||
|
|
||||||
var pathExists = await fs.pathExists(folderPath)
|
var pathExists = await fs.pathExists(folderPath)
|
||||||
if (!pathExists) {
|
if (!pathExists) {
|
||||||
@ -160,39 +153,38 @@ async function scanRootDir(folder, serverSettings = {}) {
|
|||||||
|
|
||||||
var fileItems = await recurseFiles(folderPath)
|
var fileItems = await recurseFiles(folderPath)
|
||||||
|
|
||||||
var audiobookGrouping = groupFileItemsIntoBooks(fileItems)
|
var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(fileItems)
|
||||||
|
|
||||||
if (!Object.keys(audiobookGrouping).length) {
|
if (!Object.keys(libraryItemGrouping).length) {
|
||||||
Logger.error('Root path has no books', fileItems.length)
|
Logger.error('Root path has no media folders', fileItems.length)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
var audiobooks = []
|
var items = []
|
||||||
for (const audiobookPath in audiobookGrouping) {
|
for (const libraryItemPath in libraryItemGrouping) {
|
||||||
var audiobookData = getAudiobookDataFromDir(folderPath, audiobookPath, parseSubtitle)
|
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
|
||||||
|
|
||||||
var fileObjs = await cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
|
var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemData.relPath, libraryItemGrouping[libraryItemPath])
|
||||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
|
||||||
audiobooks.push({
|
items.push({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
ino: audiobookFolderStats.ino,
|
ino: libraryItemFolderStats.ino,
|
||||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
||||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
||||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
||||||
...audiobookData,
|
...libraryItemData,
|
||||||
audioFiles: fileObjs.filter(f => f.filetype === 'audio'),
|
libraryFiles: fileObjs
|
||||||
otherFiles: fileObjs.filter(f => f.filetype !== 'audio')
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return audiobooks
|
return items
|
||||||
}
|
}
|
||||||
module.exports.scanRootDir = scanRootDir
|
module.exports.scanFolder = scanFolder
|
||||||
|
|
||||||
// Input relative filepath, output all details that can be parsed
|
// Input relative filepath, output all details that can be parsed
|
||||||
function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
||||||
dir = dir.replace(/\\/g, '/')
|
relPath = relPath.replace(/\\/g, '/')
|
||||||
var splitDir = dir.split('/')
|
var splitDir = relPath.split('/')
|
||||||
|
|
||||||
// Audio files will always be in the directory named for the title
|
// Audio files will always be in the directory named for the title
|
||||||
var title = splitDir.pop()
|
var title = splitDir.pop()
|
||||||
@ -244,7 +236,6 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var publishYear = null
|
var publishYear = null
|
||||||
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
||||||
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
||||||
@ -270,58 +261,52 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author,
|
mediaMetadata: {
|
||||||
title,
|
author,
|
||||||
subtitle,
|
title,
|
||||||
series,
|
subtitle,
|
||||||
volumeNumber,
|
series,
|
||||||
publishYear,
|
sequence: volumeNumber,
|
||||||
path: dir, // relative audiobook path i.e. /Author Name/Book Name/..
|
publishYear,
|
||||||
fullPath: Path.posix.join(folderPath, dir) // i.e. /audiobook/Author Name/Book Name/..
|
},
|
||||||
|
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
|
||||||
|
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAudiobookFileData(folder, audiobookPath, serverSettings = {}) {
|
function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) {
|
||||||
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
var parseSubtitle = !!serverSettings.scannerParseSubtitle
|
||||||
|
return getBookDataFromDir(folderPath, relPath, parseSubtitle)
|
||||||
|
}
|
||||||
|
|
||||||
var fileItems = await recurseFiles(audiobookPath, folder.fullPath)
|
|
||||||
|
|
||||||
audiobookPath = audiobookPath.replace(/\\/g, '/')
|
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
|
||||||
|
var fileItems = await recurseFiles(libraryItemPath, folder.fullPath)
|
||||||
|
|
||||||
|
libraryItemPath = libraryItemPath.replace(/\\/g, '/')
|
||||||
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
||||||
|
|
||||||
var audiobookDir = audiobookPath.replace(folderFullPath, '').slice(1)
|
var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1)
|
||||||
var audiobookData = getAudiobookDataFromDir(folderFullPath, audiobookDir, parseSubtitle)
|
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings)
|
||||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path)
|
||||||
var audiobook = {
|
var libraryItem = {
|
||||||
ino: audiobookFolderStats.ino,
|
ino: libraryItemDirStats.ino,
|
||||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
mtimeMs: libraryItemDirStats.mtimeMs || 0,
|
||||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
ctimeMs: libraryItemDirStats.ctimeMs || 0,
|
||||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
birthtimeMs: libraryItemDirStats.birthtimeMs || 0,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
...audiobookData,
|
...libraryItemData,
|
||||||
audioFiles: [],
|
libraryFiles: []
|
||||||
otherFiles: []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < fileItems.length; i++) {
|
for (let i = 0; i < fileItems.length; i++) {
|
||||||
var fileItem = fileItems[i]
|
var fileItem = fileItems[i]
|
||||||
|
var newLibraryFile = new LibraryFile()
|
||||||
var fileStatData = await getFileTimestampsWithIno(fileItem.fullpath)
|
// fileItem.path is the relative path
|
||||||
var fileObj = {
|
await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
|
||||||
filetype: getFileType(fileItem.extension),
|
libraryItem.libraryFiles.push(newLibraryFile)
|
||||||
filename: fileItem.name,
|
|
||||||
path: fileItem.path,
|
|
||||||
fullPath: fileItem.fullpath,
|
|
||||||
ext: fileItem.extension,
|
|
||||||
...fileStatData
|
|
||||||
}
|
|
||||||
if (fileObj.filetype === 'audio') {
|
|
||||||
audiobook.audioFiles.push(fileObj)
|
|
||||||
} else {
|
|
||||||
audiobook.otherFiles.push(fileObj)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return audiobook
|
return libraryItem
|
||||||
}
|
}
|
||||||
module.exports.getAudiobookFileData = getAudiobookFileData
|
module.exports.getLibraryItemFileData = getLibraryItemFileData
|
Loading…
Reference in New Issue
Block a user