mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Missing audiobooks flagged not deleted, fix close progress loop on stream errors, clickable download toast, consolidate duplicate track error log, improved scanner to ignore non-audio files
This commit is contained in:
parent
0851a1e71e
commit
db01db3a2b
@ -14,7 +14,7 @@
|
|||||||
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||||
|
|
||||||
<div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist">
|
<div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist">
|
||||||
<div v-show="!isSelectionMode" class="h-full flex items-center justify-center">
|
<div v-show="!isSelectionMode && !isMissing" class="h-full flex items-center justify-center">
|
||||||
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
|
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||||
</div>
|
</div>
|
||||||
@ -132,7 +132,10 @@ export default {
|
|||||||
return this.userProgress ? !!this.userProgress.isRead : false
|
return this.userProgress ? !!this.userProgress.isRead : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
hasMissingParts() {
|
||||||
return this.audiobook.hasMissingParts
|
return this.audiobook.hasMissingParts
|
||||||
@ -141,6 +144,7 @@ export default {
|
|||||||
return this.audiobook.hasInvalidParts
|
return this.audiobook.hasInvalidParts
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
|
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.hasMissingParts) {
|
if (this.hasMissingParts) {
|
||||||
txt = `${this.hasMissingParts} missing parts.`
|
txt = `${this.hasMissingParts} missing parts.`
|
||||||
|
@ -109,6 +109,7 @@ export default {
|
|||||||
availableTabs() {
|
availableTabs() {
|
||||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||||
return this.tabs.filter((tab) => {
|
return this.tabs.filter((tab) => {
|
||||||
|
if (tab.id === 'download' && this.isMissing) return false
|
||||||
if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true
|
if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true
|
||||||
if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true
|
if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true
|
||||||
return false
|
return false
|
||||||
@ -122,6 +123,9 @@ export default {
|
|||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.selectedAudiobook.isMissing
|
||||||
|
},
|
||||||
selectedAudiobook() {
|
selectedAudiobook() {
|
||||||
return this.$store.state.selectedAudiobook || {}
|
return this.$store.state.selectedAudiobook || {}
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<th class="text-left">Filename</th>
|
<th class="text-left">Filename</th>
|
||||||
<th class="text-left">Size</th>
|
<th class="text-left">Size</th>
|
||||||
<th class="text-left">Duration</th>
|
<th class="text-left">Duration</th>
|
||||||
<th v-if="userCanDownload" class="text-center">Download</th>
|
<th v-if="showDownload" class="text-center">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracks">
|
<template v-for="track in tracks">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="font-mono text-center">
|
<td v-if="showDownload" class="font-mono text-center">
|
||||||
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -64,6 +64,12 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
|
showDownload() {
|
||||||
|
return this.userCanDownload && !this.isMissing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :class="className" @click="clickBtn">
|
<button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :disabled="disabled" :class="className" @click="clickBtn">
|
||||||
<span class="material-icons icon-text">{{ icon }}</span>
|
<span class="material-icons icon-text">{{ icon }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@ -39,6 +39,9 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
button.icon-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
button.icon-btn::before {
|
button.icon-btn::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -14,7 +14,8 @@ export default {
|
|||||||
direction: {
|
direction: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'right'
|
default: 'right'
|
||||||
}
|
},
|
||||||
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -25,6 +26,11 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
text() {
|
text() {
|
||||||
this.updateText()
|
this.updateText()
|
||||||
|
},
|
||||||
|
disabled(newVal) {
|
||||||
|
if (newVal && this.isShowing) {
|
||||||
|
this.hideTooltip()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -81,6 +87,7 @@ export default {
|
|||||||
tooltip.style.left = left + 'px'
|
tooltip.style.left = left + 'px'
|
||||||
},
|
},
|
||||||
showTooltip() {
|
showTooltip() {
|
||||||
|
if (this.disabled) return
|
||||||
if (!this.tooltip) {
|
if (!this.tooltip) {
|
||||||
this.createTooltip()
|
this.createTooltip()
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ export default {
|
|||||||
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
||||||
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
||||||
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
||||||
|
if (results.missing) scanResultMsgs.push(`${results.missing} missing`)
|
||||||
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
||||||
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
||||||
}
|
}
|
||||||
@ -128,19 +129,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloadToastClick(download) {
|
downloadToastClick(download) {
|
||||||
console.log('Downlaod ready toast click', download)
|
if (!download || !download.audiobookId) {
|
||||||
// if (!download || !download.audiobookId) {
|
return console.error('Invalid download object', download)
|
||||||
// return console.error('Invalid download object', download)
|
}
|
||||||
// }
|
var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
||||||
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
if (!audiobook) {
|
||||||
// if (!audiobook) {
|
return console.error('Audiobook not found for download', download)
|
||||||
// return console.error('Audiobook not found for download', download)
|
}
|
||||||
// }
|
this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
||||||
// this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
|
||||||
},
|
},
|
||||||
downloadStarted(download) {
|
downloadStarted(download) {
|
||||||
download.status = this.$constants.DownloadStatus.PENDING
|
download.status = this.$constants.DownloadStatus.PENDING
|
||||||
download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: this.downloadToastClick })
|
download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: () => this.downloadToastClick(download) })
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
},
|
},
|
||||||
downloadReady(download) {
|
downloadReady(download) {
|
||||||
@ -149,7 +149,7 @@ export default {
|
|||||||
|
|
||||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||||
download.toastId = existingDownload.toastId
|
download.toastId = existingDownload.toastId
|
||||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: this.downloadToastClick } }, true)
|
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: () => this.downloadToastClick(download) } }, true)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(`Download "${download.filename}" is ready!`)
|
this.$toast.success(`Download "${download.filename}" is ready!`)
|
||||||
}
|
}
|
||||||
@ -163,7 +163,7 @@ export default {
|
|||||||
|
|
||||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||||
download.toastId = existingDownload.toastId
|
download.toastId = existingDownload.toastId
|
||||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
|
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: () => this.downloadToastClick(download) } }, true)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Download failed no existing download', existingDownload)
|
console.warn('Download failed no existing download', existingDownload)
|
||||||
this.$toast.error(`Download "${download.filename}" ${failedMsg}`)
|
this.$toast.error(`Download "${download.filename}" ${failedMsg}`)
|
||||||
@ -174,7 +174,7 @@ export default {
|
|||||||
var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
|
var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
|
||||||
if (existingDownload && existingDownload.toastId !== undefined) {
|
if (existingDownload && existingDownload.toastId !== undefined) {
|
||||||
download.toastId = existingDownload.toastId
|
download.toastId = existingDownload.toastId
|
||||||
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true)
|
this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: () => this.downloadToastClick(download) } }, true)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Download killed no existing download found', existingDownload)
|
console.warn('Download killed no existing download found', existingDownload)
|
||||||
this.$toast.error(`Download "${download.filename}" was terminated`)
|
this.$toast.error(`Download "${download.filename}" was terminated`)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -31,17 +31,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-4">
|
<div class="flex items-center pt-4">
|
||||||
<ui-btn :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
<ui-btn v-if="!isMissing" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ streaming ? 'Streaming' : 'Play' }}
|
{{ streaming ? 'Streaming' : 'Play' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
<ui-btn v-else color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
||||||
|
Missing
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
<ui-tooltip v-if="userCanUpdate" text="Edit" direction="top">
|
<ui-tooltip v-if="userCanUpdate" text="Edit" direction="top">
|
||||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="userCanDownload" text="Download" direction="top">
|
<ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top">
|
||||||
<ui-icon-btn icon="download" class="mx-0.5" @click="downloadClick" />
|
<ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :text="isRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top">
|
<ui-tooltip :text="isRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top">
|
||||||
@ -152,6 +156,9 @@ export default {
|
|||||||
})
|
})
|
||||||
return chunks
|
return chunks
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.audiobook.missingParts || []
|
return this.audiobook.missingParts || []
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -9,7 +9,6 @@ const { comparePaths, getIno } = require('./utils/index')
|
|||||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||||
const { ScanResult } = require('./utils/constants')
|
const { ScanResult } = require('./utils/constants')
|
||||||
|
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
|
||||||
this.AudiobookPath = AUDIOBOOK_PATH
|
this.AudiobookPath = AUDIOBOOK_PATH
|
||||||
@ -71,6 +70,7 @@ class Scanner {
|
|||||||
if (existingAudiobook) {
|
if (existingAudiobook) {
|
||||||
|
|
||||||
// REMOVE: No valid audio files
|
// REMOVE: No valid audio files
|
||||||
|
// TODO: Label as incomplete, do not actually delete
|
||||||
if (!audiobookData.audioFiles.length) {
|
if (!audiobookData.audioFiles.length) {
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
|
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
|
||||||
|
|
||||||
@ -109,8 +109,8 @@ class Scanner {
|
|||||||
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
|
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// REMOVE: No valid audio tracks
|
// REMOVE: No valid audio tracks
|
||||||
|
// TODO: Label as incomplete, do not actually delete
|
||||||
if (!existingAudiobook.tracks.length) {
|
if (!existingAudiobook.tracks.length) {
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
|
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
|
||||||
|
|
||||||
@ -135,6 +135,12 @@ class Scanner {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingAudiobook.isMissing) {
|
||||||
|
existingAudiobook.isMissing = false
|
||||||
|
hasUpdates = true
|
||||||
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
existingAudiobook.setChapters()
|
existingAudiobook.setChapters()
|
||||||
|
|
||||||
@ -173,23 +179,24 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scan() {
|
async scan() {
|
||||||
|
// TODO: This temporary fix from pre-release should be removed soon, including the "fixRelativePath" and "checkUpdateInos"
|
||||||
// TEMP - fix relative file paths
|
// TEMP - fix relative file paths
|
||||||
// TEMP - update ino for each audiobook
|
// TEMP - update ino for each audiobook
|
||||||
if (this.audiobooks.length) {
|
// if (this.audiobooks.length) {
|
||||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
// for (let i = 0; i < this.audiobooks.length; i++) {
|
||||||
var ab = this.audiobooks[i]
|
// var ab = this.audiobooks[i]
|
||||||
var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
|
// var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
|
||||||
|
|
||||||
// Update ino if an audio file has the same ino as the audiobook
|
// // Update ino if an audio file has the same ino as the audiobook
|
||||||
var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino)
|
// var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino)
|
||||||
if (shouldUpdateIno) {
|
// if (shouldUpdateIno) {
|
||||||
await ab.checkUpdateInos()
|
// await ab.checkUpdateInos()
|
||||||
}
|
// }
|
||||||
if (shouldUpdate) {
|
// if (shouldUpdate) {
|
||||||
await this.db.updateAudiobook(ab)
|
// await this.db.updateAudiobook(ab)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const scanStart = Date.now()
|
const scanStart = Date.now()
|
||||||
var audiobookDataFound = await scanRootDir(this.AudiobookPath, this.db.serverSettings)
|
var audiobookDataFound = await scanRootDir(this.AudiobookPath, this.db.serverSettings)
|
||||||
@ -205,18 +212,21 @@ class Scanner {
|
|||||||
var scanResults = {
|
var scanResults = {
|
||||||
removed: 0,
|
removed: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
added: 0
|
added: 0,
|
||||||
|
missing: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for removed audiobooks
|
// Check for removed audiobooks
|
||||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||||
var dataFound = audiobookDataFound.find(abd => abd.ino === this.audiobooks[i].ino)
|
var audiobook = this.audiobooks[i]
|
||||||
|
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino)
|
||||||
if (!dataFound) {
|
if (!dataFound) {
|
||||||
Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`)
|
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
|
||||||
var audiobookJSON = this.audiobooks[i].toJSONMinified()
|
audiobook.isMissing = true
|
||||||
await this.db.removeEntity('audiobook', this.audiobooks[i].id)
|
audiobook.lastUpdate = Date.now()
|
||||||
scanResults.removed++
|
scanResults.missing++
|
||||||
this.emitter('audiobook_removed', audiobookJSON)
|
await this.db.updateAudiobook(audiobook)
|
||||||
|
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||||
}
|
}
|
||||||
if (this.cancelScan) {
|
if (this.cancelScan) {
|
||||||
this.cancelScan = false
|
this.cancelScan = false
|
||||||
@ -247,7 +257,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
||||||
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
||||||
return scanResults
|
return scanResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,6 @@ class Server {
|
|||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// Used in development to set-up streams without authentication
|
// Used in development to set-up streams without authentication
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.use('/test-hls', this.hlsController.router)
|
app.use('/test-hls', this.hlsController.router)
|
||||||
|
@ -28,6 +28,9 @@ class Audiobook {
|
|||||||
this.book = null
|
this.book = null
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
|
|
||||||
|
// Audiobook was scanned and not found
|
||||||
|
this.isMissing = false
|
||||||
|
|
||||||
if (audiobook) {
|
if (audiobook) {
|
||||||
this.construct(audiobook)
|
this.construct(audiobook)
|
||||||
}
|
}
|
||||||
@ -55,6 +58,8 @@ class Audiobook {
|
|||||||
if (audiobook.chapters) {
|
if (audiobook.chapters) {
|
||||||
this.chapters = audiobook.chapters.map(c => ({ ...c }))
|
this.chapters = audiobook.chapters.map(c => ({ ...c }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isMissing = !!audiobook.isMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
@ -127,7 +132,8 @@ class Audiobook {
|
|||||||
tracks: this.tracksToJSON(),
|
tracks: this.tracksToJSON(),
|
||||||
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
||||||
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
|
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
|
||||||
chapters: this.chapters || []
|
chapters: this.chapters || [],
|
||||||
|
isMissing: !!this.isMissing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +153,8 @@ class Audiobook {
|
|||||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
||||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
chapters: this.chapters || []
|
chapters: this.chapters || [],
|
||||||
|
isMissing: !!this.isMissing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +176,8 @@ class Audiobook {
|
|||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
tracks: this.tracksToJSON(),
|
tracks: this.tracksToJSON(),
|
||||||
chapters: this.chapters || []
|
chapters: this.chapters || [],
|
||||||
|
isMissing: !!this.isMissing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,6 +288,7 @@ class Stream extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
Logger.error('Ffmpeg Err', err.message)
|
Logger.error('Ffmpeg Err', err.message)
|
||||||
}
|
}
|
||||||
|
clearInterval(this.loop)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.ffmpeg.on('end', (stdout, stderr) => {
|
this.ffmpeg.on('end', (stdout, stderr) => {
|
||||||
@ -300,6 +301,7 @@ class Stream extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.isTranscodeComplete = true
|
this.isTranscodeComplete = true
|
||||||
this.ffmpeg = null
|
this.ffmpeg = null
|
||||||
|
clearInterval(this.loop)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.ffmpeg.run()
|
this.ffmpeg.run()
|
||||||
|
@ -89,6 +89,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var tracks = []
|
var tracks = []
|
||||||
|
var numDuplicateTracks = 0
|
||||||
|
var numInvalidTracks = 0
|
||||||
for (let i = 0; i < newAudioFiles.length; i++) {
|
for (let i = 0; i < newAudioFiles.length; i++) {
|
||||||
var audioFile = newAudioFiles[i]
|
var audioFile = newAudioFiles[i]
|
||||||
var scanData = await scan(audioFile.fullPath)
|
var scanData = await scan(audioFile.fullPath)
|
||||||
@ -118,17 +120,19 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
if (newAudioFiles.length > 1) {
|
if (newAudioFiles.length > 1) {
|
||||||
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
||||||
if (trackNumber === null) {
|
if (trackNumber === null) {
|
||||||
Logger.error('[AudioFileScanner] Invalid track number for', audioFile.filename)
|
Logger.debug('[AudioFileScanner] Invalid track number for', audioFile.filename)
|
||||||
audioFile.invalid = true
|
audioFile.invalid = true
|
||||||
audioFile.error = 'Failed to get track number'
|
audioFile.error = 'Failed to get track number'
|
||||||
|
numInvalidTracks++
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.find(t => t.index === trackNumber)) {
|
if (tracks.find(t => t.index === trackNumber)) {
|
||||||
Logger.error('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
||||||
audioFile.invalid = true
|
audioFile.invalid = true
|
||||||
audioFile.error = 'Duplicate track number'
|
audioFile.error = 'Duplicate track number'
|
||||||
|
numDuplicateTracks++
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,6 +145,13 @@ async function scanAudioFiles(audiobook, newAudioFiles) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (numDuplicateTracks > 0) {
|
||||||
|
Logger.warn(`[AudioFileScanner] ${numDuplicateTracks} Duplicate tracks for "${audiobook.title}"`)
|
||||||
|
}
|
||||||
|
if (numInvalidTracks > 0) {
|
||||||
|
Logger.error(`[AudioFileScanner] ${numDuplicateTracks} Invalid tracks for "${audiobook.title}"`)
|
||||||
|
}
|
||||||
|
|
||||||
tracks.sort((a, b) => a.index - b.index)
|
tracks.sort((a, b) => a.index - b.index)
|
||||||
audiobook.audioFiles.sort((a, b) => {
|
audiobook.audioFiles.sort((a, b) => {
|
||||||
var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0
|
var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0
|
||||||
|
@ -19,11 +19,17 @@ function getPaths(path) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAudioFile(path) {
|
||||||
|
if (!path) return false
|
||||||
|
var ext = Path.extname(path)
|
||||||
|
if (!ext) return false
|
||||||
|
return AUDIO_FORMATS.includes(ext.slice(1).toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
function groupFilesIntoAudiobookPaths(paths) {
|
function groupFilesIntoAudiobookPaths(paths) {
|
||||||
// Step 1: Normalize path, Remove leading "/", Filter out files in root dir
|
// Step 1: Normalize path, Remove leading "/", Filter out files in root dir
|
||||||
var pathsFiltered = paths.map(path => Path.normalize(path.slice(1))).filter(path => Path.parse(path).dir)
|
var pathsFiltered = paths.map(path => Path.normalize(path.slice(1))).filter(path => Path.parse(path).dir)
|
||||||
|
|
||||||
|
|
||||||
// Step 2: Sort by least number of directories
|
// Step 2: Sort by least number of directories
|
||||||
pathsFiltered.sort((a, b) => {
|
pathsFiltered.sort((a, b) => {
|
||||||
var pathsA = Path.dirname(a).split(Path.sep).length
|
var pathsA = Path.dirname(a).split(Path.sep).length
|
||||||
@ -31,25 +37,55 @@ function groupFilesIntoAudiobookPaths(paths) {
|
|||||||
return pathsA - pathsB
|
return pathsA - pathsB
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 3: Group into audiobooks
|
// Step 2.5: Seperate audio files and other files
|
||||||
|
var audioFilePaths = []
|
||||||
|
var otherFilePaths = []
|
||||||
|
pathsFiltered.forEach(path => {
|
||||||
|
if (isAudioFile(path)) audioFilePaths.push(path)
|
||||||
|
else otherFilePaths.push(path)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Step 3: Group audio files in audiobooks
|
||||||
var audiobookGroup = {}
|
var audiobookGroup = {}
|
||||||
pathsFiltered.forEach((path) => {
|
audioFilePaths.forEach((path) => {
|
||||||
var dirparts = Path.dirname(path).split(Path.sep)
|
var dirparts = Path.dirname(path).split(Path.sep)
|
||||||
var numparts = dirparts.length
|
var numparts = dirparts.length
|
||||||
var _path = ''
|
var _path = ''
|
||||||
|
|
||||||
|
// Iterate over directories in path
|
||||||
for (let i = 0; i < numparts; i++) {
|
for (let i = 0; i < numparts; i++) {
|
||||||
var dirpart = dirparts.shift()
|
var dirpart = dirparts.shift()
|
||||||
_path = Path.join(_path, dirpart)
|
_path = Path.join(_path, dirpart)
|
||||||
if (audiobookGroup[_path]) {
|
|
||||||
|
|
||||||
|
if (audiobookGroup[_path]) { // Directory already has files, add file
|
||||||
var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path))
|
var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path))
|
||||||
audiobookGroup[_path].push(relpath)
|
audiobookGroup[_path].push(relpath)
|
||||||
return
|
return
|
||||||
} else if (!dirparts.length) {
|
} else if (!dirparts.length) { // This is the last directory, create group
|
||||||
audiobookGroup[_path] = [Path.basename(path)]
|
audiobookGroup[_path] = [Path.basename(path)]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Step 4: Add other files into audiobook groups
|
||||||
|
otherFilePaths.forEach((path) => {
|
||||||
|
var dirparts = Path.dirname(path).split(Path.sep)
|
||||||
|
var numparts = dirparts.length
|
||||||
|
var _path = ''
|
||||||
|
|
||||||
|
// Iterate over directories in path
|
||||||
|
for (let i = 0; i < numparts; i++) {
|
||||||
|
var dirpart = dirparts.shift()
|
||||||
|
_path = Path.join(_path, dirpart)
|
||||||
|
if (audiobookGroup[_path]) { // Directory is audiobook group
|
||||||
|
var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path))
|
||||||
|
audiobookGroup[_path].push(relpath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
return audiobookGroup
|
return audiobookGroup
|
||||||
}
|
}
|
||||||
module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths
|
module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths
|
||||||
|
Loading…
Reference in New Issue
Block a user