mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-01 16:30:39 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecc425d89d | |||
| 289b3e9f94 | |||
| 74a68a4557 | |||
| 42604331ff | |||
| 428a515c6a | |||
| c81b12f459 | |||
| 779d22bf55 | |||
| 295c6b0c74 | |||
| aa50cc2d81 | |||
| eb109c398f | |||
| 4e7d2ddc58 | |||
| a5b10aac96 | |||
| fcbccaeb4e |
@@ -5,6 +5,7 @@
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
<span v-if="volumeNumber">#{{ volumeNumber }} </span>{{ displayTitle }}
|
||||
@@ -62,16 +63,20 @@
|
||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series name overlay -->
|
||||
<div v-if="booksInSeries && audiobook && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
||||
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error widget -->
|
||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Volume number -->
|
||||
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||
</div>
|
||||
@@ -204,6 +209,8 @@ export default {
|
||||
return this.authorFL
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._audiobook.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._audiobook.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._audiobook.addedAt)
|
||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this._audiobook.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._audiobook.size)
|
||||
|
||||
@@ -59,6 +59,14 @@ export default {
|
||||
{
|
||||
text: 'Size',
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: 'File Birthtime',
|
||||
value: 'birthtimeMs'
|
||||
},
|
||||
{
|
||||
text: 'File Modified',
|
||||
value: 'mtimeMs'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ export default {
|
||||
}).map(ab => this.cleanBook(ab, index++))
|
||||
return {
|
||||
books,
|
||||
invalidBooks,
|
||||
ignoredFiles
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -57,16 +57,6 @@ export default {
|
||||
this.$store.commit('setDeveloperMode', value)
|
||||
this.$toast.info(`Developer Mode ${value ? 'Enabled' : 'Disabled'}`)
|
||||
}
|
||||
// saveMetadataComplete(result) {
|
||||
// this.savingMetadata = false
|
||||
// if (!result) return
|
||||
// this.$toast.success(`Metadata saved for ${result.success} audiobooks`)
|
||||
// },
|
||||
// saveMetadataFiles() {
|
||||
// this.savingMetadata = true
|
||||
// this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
|
||||
// this.$root.socket.emit('save_metadata')
|
||||
// }
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,20 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
|
||||
<ui-tooltip :text="tooltips.coverDestination">
|
||||
<ui-toggle-switch v-model="newServerSettings.storeCoverWithBook" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithBook', val)" />
|
||||
<ui-tooltip :text="tooltips.storeCoverWithBook">
|
||||
<p class="pl-4 text-lg">
|
||||
Store covers with audiobook
|
||||
Store covers with book
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithBook" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithBook', val)" />
|
||||
<ui-tooltip :text="tooltips.storeMetadataWithBook">
|
||||
<p class="pl-4 text-lg">
|
||||
Store metadata with book
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
@@ -172,7 +182,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isResettingAudiobooks: false,
|
||||
storeCoversInAudiobookDir: false,
|
||||
updatingServerSettings: false,
|
||||
useSquareBookCovers: false,
|
||||
useAlternativeBookshelfView: false,
|
||||
@@ -182,10 +191,11 @@ export default {
|
||||
scannerDisableWatcher: 'Disables the automatic adding/updating of audiobooks when file changes are detected. *Requires server restart',
|
||||
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
||||
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
||||
scannerParseSubtitle: 'Extract subtitles from audiobook directory names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||
scannerFindCovers: 'If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time',
|
||||
bookshelfView: 'Alternative bookshelf view that shows title & author under book covers',
|
||||
coverDestination: 'By default covers are stored in /metadata/books, enabling this setting will store covers inside your audiobooks directory. Only one file named "cover" will be kept.',
|
||||
storeCoverWithBook: 'By default covers are stored in /metadata/books, enabling this setting will store covers in the books folder. Only one file named "cover" will be kept',
|
||||
storeMetadataWithBook: 'By default metadata files are stored in /metadata/books, enabling this setting will store metadata files in the books folder. Uses .abs file extension',
|
||||
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
|
||||
}
|
||||
}
|
||||
@@ -226,12 +236,6 @@ export default {
|
||||
scannerCoverProvider: val
|
||||
})
|
||||
},
|
||||
updateCoverStorageDestination(val) {
|
||||
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
|
||||
this.updateServerSettings({
|
||||
coverDestination: this.newServerSettings.coverDestination
|
||||
})
|
||||
},
|
||||
updateBookCoverAspectRatio(val) {
|
||||
this.updateServerSettings({
|
||||
coverAspectRatio: val ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD
|
||||
@@ -263,8 +267,6 @@ export default {
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
|
||||
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
|
||||
|
||||
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
||||
|
||||
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||
|
||||
@@ -14,11 +14,6 @@ const DownloadStatus = {
|
||||
FAILED: 3
|
||||
}
|
||||
|
||||
const CoverDestination = {
|
||||
METADATA: 0,
|
||||
AUDIOBOOK: 1
|
||||
}
|
||||
|
||||
const BookCoverAspectRatio = {
|
||||
STANDARD: 0,
|
||||
SQUARE: 1
|
||||
@@ -32,7 +27,6 @@ const BookshelfView = {
|
||||
const Constants = {
|
||||
SupportedFileTypes,
|
||||
DownloadStatus,
|
||||
CoverDestination,
|
||||
BookCoverAspectRatio,
|
||||
BookshelfView
|
||||
}
|
||||
|
||||
Generated
+1
-415
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.6.66",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -77,12 +77,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"optional": true
|
||||
},
|
||||
"aborter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aborter/-/aborter-1.1.0.tgz",
|
||||
@@ -97,23 +91,6 @@
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
|
||||
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||
"optional": true
|
||||
},
|
||||
"archiver": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz",
|
||||
@@ -169,33 +146,6 @@
|
||||
"is-primitive": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
|
||||
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"array-back": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
|
||||
@@ -343,12 +293,6 @@
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"optional": true
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
@@ -357,12 +301,6 @@
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"optional": true
|
||||
},
|
||||
"command-line-args": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
@@ -395,12 +333,6 @@
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"optional": true
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||
@@ -489,23 +421,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"optional": true
|
||||
},
|
||||
"defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||
"optional": true
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
@@ -516,12 +436,6 @@
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
|
||||
"optional": true
|
||||
},
|
||||
"dicer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz",
|
||||
@@ -598,16 +512,6 @@
|
||||
"base64-arraybuffer": "0.1.4"
|
||||
}
|
||||
},
|
||||
"epub": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/epub/-/epub-1.2.1.tgz",
|
||||
"integrity": "sha512-2GDDr2qcH3dvwX1lgwCQ3gki0CwwrxELLI005SauhT2TacJUiDqZrQuGuOSWEYIHX6ox5kXHpn1ZjsHqkNCb+g==",
|
||||
"requires": {
|
||||
"adm-zip": "^0.4.11",
|
||||
"xml2js": "^0.4.23",
|
||||
"zipfile": "^0.5.11"
|
||||
}
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -744,36 +648,11 @@
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^1.0.1",
|
||||
"strip-ansi": "^3.0.1",
|
||||
"wide-align": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
@@ -819,12 +698,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
|
||||
"optional": true
|
||||
},
|
||||
"html-entities": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz",
|
||||
@@ -869,15 +742,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
|
||||
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"image-type": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
|
||||
@@ -900,12 +764,6 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"optional": true
|
||||
},
|
||||
"ip": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
|
||||
@@ -916,15 +774,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-primitive": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
|
||||
@@ -1163,40 +1012,6 @@
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
@@ -1215,40 +1030,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
|
||||
"optional": true
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
|
||||
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
@@ -1275,39 +1056,11 @@
|
||||
"resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz",
|
||||
"integrity": "sha512-2LNTLStz2hw/urwo4xJ00TIOvthgepcl3tF4HB8BWnhJ4nhJ7S08YThapBHkGLYV+GUuY9pML/kX76+dqY2iUg=="
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz",
|
||||
"integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"tar": "^4"
|
||||
}
|
||||
},
|
||||
"node-stream-zip": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
|
||||
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -1318,50 +1071,6 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
|
||||
"integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
|
||||
"optional": true
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
|
||||
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1",
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
"gauge": "~2.7.3",
|
||||
"set-blocking": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -1383,28 +1092,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"optional": true
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
||||
"optional": true
|
||||
},
|
||||
"osenv": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
@@ -1555,18 +1242,6 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
}
|
||||
},
|
||||
"read-chunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.1.0.tgz",
|
||||
@@ -1622,15 +1297,6 @@
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"ripstat": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ripstat/-/ripstat-1.1.1.tgz",
|
||||
@@ -1721,12 +1387,6 @@
|
||||
"send": "0.17.1"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
||||
"optional": true
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
@@ -1854,17 +1514,6 @@
|
||||
"@babel/runtime": "^7.14.0"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
"strip-ansi": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@@ -1873,44 +1522,6 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"optional": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.19",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
|
||||
"integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.4",
|
||||
"fs-minipass": "^1.2.7",
|
||||
"minipass": "^2.9.0",
|
||||
"minizlib": "^1.3.3",
|
||||
"mkdirp": "^0.5.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"yallist": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
@@ -1996,15 +1607,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
}
|
||||
},
|
||||
"with-open-file": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
|
||||
@@ -2044,12 +1646,6 @@
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"optional": true
|
||||
},
|
||||
"zip-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
|
||||
@@ -2059,16 +1655,6 @@
|
||||
"compress-commons": "^4.1.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"zipfile": {
|
||||
"version": "0.5.12",
|
||||
"resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz",
|
||||
"integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "~2.10.0",
|
||||
"node-pre-gyp": "~0.10.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -32,7 +32,6 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"command-line-args": "^5.2.0",
|
||||
"date-and-time": "^2.0.1",
|
||||
"epub": "^1.2.1",
|
||||
"express": "^4.17.1",
|
||||
"express-fileupload": "^1.2.1",
|
||||
"express-rate-limit": "^5.3.0",
|
||||
@@ -56,4 +55,4 @@
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,11 @@ Audiobookshelf is a self-hosted audiobook server for managing and playing your a
|
||||
* Multi-user support w/ custom permissions
|
||||
* Keeps progress per user and syncs across devices
|
||||
* Auto-detects library updates, no need to re-scan
|
||||
* Upload full audiobooks and covers
|
||||
* Upload audiobooks w/ bulk upload drag and drop folders
|
||||
* Backup your metadata + automated daily backups
|
||||
* Progressive Web App (PWA)
|
||||
* Chromecast support on the web app
|
||||
* Fetch metadata and cover art from several sources
|
||||
|
||||
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const AuthorFinder = require('./AuthorFinder')
|
||||
const FileSystemController = require('./controllers/FileSystemController')
|
||||
|
||||
class ApiController {
|
||||
constructor(MetadataPath, db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
||||
constructor(db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
this.auth = auth
|
||||
this.scanner = scanner
|
||||
@@ -31,10 +31,9 @@ class ApiController {
|
||||
this.cacheManager = cacheManager
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
this.MetadataPath = MetadataPath
|
||||
|
||||
this.bookFinder = new BookFinder()
|
||||
this.authorFinder = new AuthorFinder(this.MetadataPath)
|
||||
this.authorFinder = new AuthorFinder()
|
||||
|
||||
this.router = express()
|
||||
this.init()
|
||||
@@ -287,7 +286,7 @@ class ApiController {
|
||||
this.backupManager.updateCronSchedule()
|
||||
}
|
||||
|
||||
await this.db.updateEntity('settings', this.db.serverSettings)
|
||||
await this.db.updateServerSettings()
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
|
||||
@@ -7,9 +7,8 @@ const Audnexus = require('./providers/Audnexus')
|
||||
const { downloadFile } = require('./utils/fileUtils')
|
||||
|
||||
class AuthorFinder {
|
||||
constructor(MetadataPath) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.AuthorPath = Path.join(MetadataPath, 'authors')
|
||||
constructor() {
|
||||
this.AuthorPath = Path.join(global.MetadataPath, 'authors')
|
||||
|
||||
this.audnexus = new Audnexus()
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ const Logger = require('./Logger')
|
||||
const Backup = require('./objects/Backup')
|
||||
|
||||
class BackupManager {
|
||||
constructor(MetadataPath, Uid, Gid, db) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.BackupPath = Path.join(this.MetadataPath, 'backups')
|
||||
constructor(Uid, Gid, db) {
|
||||
this.BackupPath = Path.join(global.MetadataPath, 'backups')
|
||||
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books')
|
||||
|
||||
this.Uid = Uid
|
||||
this.Gid = Gid
|
||||
@@ -142,10 +142,9 @@ class BackupManager {
|
||||
return
|
||||
}
|
||||
const zip = new StreamZip.async({ file: backup.fullPath })
|
||||
await zip.extract('config/', this.db.ConfigPath)
|
||||
await zip.extract('config/', global.ConfigPath)
|
||||
if (backup.backupMetadataCovers) {
|
||||
var metadataBooksPath = Path.join(this.MetadataPath, 'books')
|
||||
await zip.extract('metadata-books/', metadataBooksPath)
|
||||
await zip.extract('metadata-books/', this.MetadataBooksPath)
|
||||
}
|
||||
await this.db.reinit()
|
||||
socket.emit('apply_backup_complete', true)
|
||||
@@ -157,7 +156,7 @@ class BackupManager {
|
||||
var lastBackup = this.backups.shift()
|
||||
|
||||
const zip = new StreamZip.async({ file: lastBackup.fullPath })
|
||||
await zip.extract('config/', this.db.ConfigPath)
|
||||
await zip.extract('config/', global.ConfigPath)
|
||||
console.log('Set Last Backup')
|
||||
await this.db.reinit()
|
||||
}
|
||||
@@ -196,7 +195,7 @@ class BackupManager {
|
||||
async runBackup() {
|
||||
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
|
||||
Logger.info(`[BackupManager] Running Backup`)
|
||||
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? Path.join(this.MetadataPath, 'books') : null
|
||||
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? this.MetadataBooksPath : null
|
||||
|
||||
var newBackup = new Backup()
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ const Logger = require('./Logger')
|
||||
const { resizeImage } = require('./utils/ffmpegHelpers')
|
||||
|
||||
class CacheManager {
|
||||
constructor(MetadataPath) {
|
||||
this.MetadataPath = MetadataPath
|
||||
this.CachePath = Path.join(this.MetadataPath, 'cache')
|
||||
constructor() {
|
||||
this.CachePath = Path.join(global.MetadataPath, 'cache')
|
||||
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
||||
}
|
||||
|
||||
|
||||
@@ -6,21 +6,18 @@ const readChunk = require('read-chunk')
|
||||
const imageType = require('image-type')
|
||||
|
||||
const globals = require('./utils/globals')
|
||||
const { CoverDestination } = require('./utils/constants')
|
||||
const { downloadFile } = require('./utils/fileUtils')
|
||||
|
||||
class CoverController {
|
||||
constructor(db, cacheManager, MetadataPath, AudiobookPath) {
|
||||
constructor(db, cacheManager) {
|
||||
this.db = db
|
||||
this.cacheManager = cacheManager
|
||||
|
||||
this.MetadataPath = MetadataPath.replace(/\\/g, '/')
|
||||
this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books')
|
||||
this.AudiobookPath = AudiobookPath
|
||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||
}
|
||||
|
||||
getCoverDirectory(audiobook) {
|
||||
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
|
||||
if (this.db.serverSettings.storeCoverWithBook) {
|
||||
return {
|
||||
fullPath: audiobook.fullPath,
|
||||
relPath: '/s/book/' + audiobook.id
|
||||
|
||||
+54
-14
@@ -12,17 +12,14 @@ const Author = require('./objects/Author')
|
||||
const ServerSettings = require('./objects/ServerSettings')
|
||||
|
||||
class Db {
|
||||
constructor(ConfigPath, AudiobookPath) {
|
||||
this.ConfigPath = ConfigPath
|
||||
this.AudiobookPath = AudiobookPath
|
||||
|
||||
this.AudiobooksPath = Path.join(ConfigPath, 'audiobooks')
|
||||
this.UsersPath = Path.join(ConfigPath, 'users')
|
||||
this.SessionsPath = Path.join(ConfigPath, 'sessions')
|
||||
this.LibrariesPath = Path.join(ConfigPath, 'libraries')
|
||||
this.SettingsPath = Path.join(ConfigPath, 'settings')
|
||||
this.CollectionsPath = Path.join(ConfigPath, 'collections')
|
||||
this.AuthorsPath = Path.join(ConfigPath, 'authors')
|
||||
constructor() {
|
||||
this.AudiobooksPath = Path.join(global.ConfigPath, 'audiobooks')
|
||||
this.UsersPath = Path.join(global.ConfigPath, 'users')
|
||||
this.SessionsPath = Path.join(global.ConfigPath, 'sessions')
|
||||
this.LibrariesPath = Path.join(global.ConfigPath, 'libraries')
|
||||
this.SettingsPath = Path.join(global.ConfigPath, 'settings')
|
||||
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
|
||||
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
|
||||
|
||||
this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
|
||||
this.usersDb = new njodb.Database(this.UsersPath)
|
||||
@@ -88,7 +85,7 @@ class Db {
|
||||
name: 'Main',
|
||||
folder: { // Generates default folder
|
||||
id: 'audiobooks',
|
||||
fullPath: this.AudiobookPath,
|
||||
fullPath: global.AudiobookPath,
|
||||
libraryId: 'main'
|
||||
}
|
||||
})
|
||||
@@ -128,6 +125,7 @@ class Db {
|
||||
this.serverSettings = new ServerSettings()
|
||||
await this.insertEntity('settings', this.serverSettings)
|
||||
}
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -170,11 +168,19 @@ class Db {
|
||||
// Update server version in server settings
|
||||
if (this.previousVersion) {
|
||||
this.serverSettings.version = version
|
||||
await this.updateEntity('settings', this.serverSettings)
|
||||
await this.updateServerSettings()
|
||||
}
|
||||
}
|
||||
|
||||
updateAudiobook(audiobook) {
|
||||
async updateAudiobook(audiobook) {
|
||||
if (audiobook && audiobook.saveAbMetadata) {
|
||||
// TODO: Book may have updates where this save is not necessary
|
||||
// add check first if metadata update is needed
|
||||
await audiobook.saveAbMetadata()
|
||||
} else {
|
||||
Logger.error(`[Db] Invalid audiobook object passed to updateAudiobook`, audiobook)
|
||||
}
|
||||
|
||||
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
|
||||
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
||||
return true
|
||||
@@ -184,6 +190,28 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
insertAudiobook(audiobook) {
|
||||
return this.insertAudiobooks([audiobook])
|
||||
}
|
||||
|
||||
async insertAudiobooks(audiobooks) {
|
||||
// TODO: Books may have updates where this save is not necessary
|
||||
// add check first if metadata update is needed
|
||||
await Promise.all(audiobooks.map(async (ab) => {
|
||||
if (ab && ab.saveAbMetadata) return ab.saveAbMetadata()
|
||||
return null
|
||||
}))
|
||||
|
||||
return this.audiobooksDb.insert(audiobooks).then((results) => {
|
||||
Logger.debug(`[DB] Audiobooks inserted ${results.inserted}`)
|
||||
this.audiobooks = this.audiobooks.concat(audiobooks)
|
||||
return true
|
||||
}).catch((error) => {
|
||||
Logger.error(`[DB] Audiobooks insert failed ${error}`)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
updateUserStream(userId, streamId) {
|
||||
return this.usersDb.update((record) => record.id === userId, (user) => {
|
||||
user.stream = streamId
|
||||
@@ -201,6 +229,11 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
global.ServerSettings = this.serverSettings.toJSON()
|
||||
return this.updateEntity('settings', this.serverSettings)
|
||||
}
|
||||
|
||||
insertEntities(entityName, entities) {
|
||||
var entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.insert(entities).then((results) => {
|
||||
@@ -308,5 +341,12 @@ class Db {
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
// Check if server was updated and previous version was earlier than param
|
||||
checkPreviousVersionIsBefore(version) {
|
||||
if (!this.previousVersion) return false
|
||||
// true if version > previousVersion
|
||||
return version.localeCompare(this.previousVersion) >= 0
|
||||
}
|
||||
}
|
||||
module.exports = Db
|
||||
|
||||
@@ -11,14 +11,12 @@ const { writeConcatFile, writeMetadataFile } = require('./utils/ffmpegHelpers')
|
||||
const { getFileSize } = require('./utils/fileUtils')
|
||||
const TAG = 'DownloadManager'
|
||||
class DownloadManager {
|
||||
constructor(db, MetadataPath, AudiobookPath, Uid, Gid) {
|
||||
constructor(db, Uid, Gid) {
|
||||
this.Uid = Uid
|
||||
this.Gid = Gid
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
this.AudiobookPath = AudiobookPath
|
||||
|
||||
this.downloadDirPath = Path.join(this.MetadataPath, 'downloads')
|
||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||
|
||||
this.pendingDownloads = []
|
||||
this.downloads = []
|
||||
@@ -248,7 +246,7 @@ class DownloadManager {
|
||||
// Supporting old local file prefix
|
||||
var bookCoverPath = audiobook.book.cover ? audiobook.book.cover.replace(/\\/g, '/') : null
|
||||
if (!_cover && bookCoverPath && bookCoverPath.startsWith('/local')) {
|
||||
_cover = Path.posix.join(this.AudiobookPath.replace(/\\/g, '/'), _cover.replace('/local', ''))
|
||||
_cover = Path.posix.join(global.AudiobookPath, _cover.replace('/local', ''))
|
||||
Logger.debug('Local cover url', _cover)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ const fs = require('fs-extra')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
class HlsController {
|
||||
constructor(db, auth, streamManager, emitter, StreamsPath) {
|
||||
constructor(db, auth, streamManager, emitter) {
|
||||
this.db = db
|
||||
this.auth = auth
|
||||
this.streamManager = streamManager
|
||||
this.emitter = emitter
|
||||
this.StreamsPath = StreamsPath
|
||||
|
||||
this.router = express()
|
||||
this.init()
|
||||
@@ -27,7 +26,7 @@ class HlsController {
|
||||
|
||||
async streamFileRequest(req, res) {
|
||||
var streamId = req.params.stream
|
||||
var fullFilePath = Path.join(this.StreamsPath, streamId, req.params.file)
|
||||
var fullFilePath = Path.join(this.streamManager.StreamsPath, streamId, req.params.file)
|
||||
|
||||
// development test stream - ignore
|
||||
if (streamId === 'test') {
|
||||
|
||||
@@ -8,11 +8,10 @@ const Logger = require('./Logger')
|
||||
const TAG = '[LogManager]'
|
||||
|
||||
class LogManager {
|
||||
constructor(MetadataPath, db) {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
|
||||
this.logDirPath = Path.join(this.MetadataPath, 'logs')
|
||||
this.logDirPath = Path.join(global.MetadataPath, 'logs')
|
||||
this.dailyLogDirPath = Path.join(this.logDirPath, 'daily')
|
||||
|
||||
this.currentDailyLog = null
|
||||
|
||||
+34
-22
@@ -35,27 +35,35 @@ class Server {
|
||||
this.Uid = isNaN(UID) ? 0 : Number(UID)
|
||||
this.Gid = isNaN(GID) ? 0 : Number(GID)
|
||||
this.Host = '0.0.0.0'
|
||||
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
this.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
global.Uid = this.Uid
|
||||
global.Gid = this.Gid
|
||||
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
global.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
global.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
// Fix backslash if not on Windows
|
||||
if (process.platform !== 'win32') {
|
||||
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
|
||||
global.AudiobookPath = global.AudiobookPath.replace(/\\/g, '/')
|
||||
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
fs.ensureDirSync(CONFIG_PATH, 0o774)
|
||||
fs.ensureDirSync(METADATA_PATH, 0o774)
|
||||
fs.ensureDirSync(AUDIOBOOK_PATH, 0o774)
|
||||
fs.ensureDirSync(global.ConfigPath, 0o774)
|
||||
fs.ensureDirSync(global.MetadataPath, 0o774)
|
||||
fs.ensureDirSync(global.AudiobookPath, 0o774)
|
||||
|
||||
this.db = new Db(this.ConfigPath, this.AudiobookPath)
|
||||
this.db = new Db()
|
||||
this.auth = new Auth(this.db)
|
||||
this.backupManager = new BackupManager(this.MetadataPath, this.Uid, this.Gid, this.db)
|
||||
this.logManager = new LogManager(this.MetadataPath, this.db)
|
||||
this.cacheManager = new CacheManager(this.MetadataPath)
|
||||
this.watcher = new Watcher(this.AudiobookPath)
|
||||
this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath)
|
||||
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
|
||||
this.backupManager = new BackupManager(this.Uid, this.Gid, this.db)
|
||||
this.logManager = new LogManager(this.db)
|
||||
this.cacheManager = new CacheManager()
|
||||
this.watcher = new Watcher()
|
||||
this.coverController = new CoverController(this.db, this.cacheManager)
|
||||
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
||||
|
||||
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.rssFeeds = new RssFeeds(this.Port, this.db)
|
||||
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.Uid, this.Gid)
|
||||
this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.downloadManager = new DownloadManager(this.db, this.Uid, this.Gid)
|
||||
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, 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)
|
||||
|
||||
Logger.logManager = this.logManager
|
||||
@@ -126,6 +134,10 @@ class Server {
|
||||
Logger.info(`[Server] Running scan for duplicate book IDs`)
|
||||
await this.scanner.fixDuplicateIds()
|
||||
}
|
||||
// If server upgrade and last version was 1.7.0 or earlier - add abmetadata files
|
||||
// if (this.db.checkPreviousVersionIsBefore('1.7.1')) {
|
||||
// TODO: wait until stable
|
||||
// }
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
Logger.info(`[Server] Watcher is disabled`)
|
||||
@@ -155,10 +167,10 @@ class Server {
|
||||
app.use(express.static(distPath))
|
||||
|
||||
// Old static path for covers
|
||||
app.use('/local', this.authMiddleware.bind(this), express.static(this.AudiobookPath))
|
||||
app.use('/local', this.authMiddleware.bind(this), express.static(global.AudiobookPath))
|
||||
|
||||
// Metadata folder static path
|
||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(this.MetadataPath))
|
||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||
|
||||
// Downloads folder static path
|
||||
app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
|
||||
@@ -349,7 +361,7 @@ class Server {
|
||||
|
||||
// Remove unused /metadata/books/{id} folders
|
||||
async purgeMetadata() {
|
||||
var booksMetadata = Path.join(this.MetadataPath, 'books')
|
||||
var booksMetadata = Path.join(global.MetadataPath, 'books')
|
||||
var booksMetadataExists = await fs.pathExists(booksMetadata)
|
||||
if (!booksMetadataExists) return
|
||||
var foldersInBooksMetadata = await fs.readdir(booksMetadata)
|
||||
@@ -622,9 +634,9 @@ class Server {
|
||||
|
||||
const initialPayload = {
|
||||
serverSettings: this.serverSettings.toJSON(),
|
||||
audiobookPath: this.AudiobookPath,
|
||||
metadataPath: this.MetadataPath,
|
||||
configPath: this.ConfigPath,
|
||||
audiobookPath: global.AudiobookPath,
|
||||
metadataPath: global.MetadataPath,
|
||||
configPath: global.ConfigPath,
|
||||
user: client.user.toJSONForBrowser(),
|
||||
stream: client.stream || null,
|
||||
librariesScanning: this.scanner.librariesScanning,
|
||||
|
||||
@@ -5,15 +5,14 @@ const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
|
||||
class StreamManager {
|
||||
constructor(db, MetadataPath, emitter, clientEmitter) {
|
||||
constructor(db, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.MetadataPath = MetadataPath
|
||||
this.streams = []
|
||||
this.StreamsPath = Path.join(this.MetadataPath, 'streams')
|
||||
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
||||
}
|
||||
|
||||
get audiobooks() {
|
||||
@@ -68,12 +67,12 @@ class StreamManager {
|
||||
|
||||
async tempCheckStrayStreams() {
|
||||
try {
|
||||
var dirs = await fs.readdir(this.MetadataPath)
|
||||
var dirs = await fs.readdir(global.MetadataPath)
|
||||
if (!dirs || !dirs.length) return true
|
||||
|
||||
await Promise.all(dirs.map(async (dirname) => {
|
||||
if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') {
|
||||
var fullPath = Path.join(this.MetadataPath, dirname)
|
||||
var fullPath = Path.join(global.MetadataPath, dirname)
|
||||
Logger.warn(`Removing OLD Orphan Stream ${dirname}`)
|
||||
return fs.remove(fullPath)
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ class BookController {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook || !audiobook.book.cover) return res.sendStatus(404)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this audiobooks library
|
||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
||||
|
||||
@@ -11,6 +11,9 @@ class AudioFile {
|
||||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
this.addedAt = null
|
||||
|
||||
this.trackNumFromMeta = null
|
||||
@@ -51,6 +54,9 @@ class AudioFile {
|
||||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
trackNumFromMeta: this.trackNumFromMeta,
|
||||
discNumFromMeta: this.discNumFromMeta,
|
||||
@@ -82,6 +88,9 @@ class AudioFile {
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = data.addedAt
|
||||
this.manuallyVerified = !!data.manuallyVerified
|
||||
this.invalid = !!data.invalid
|
||||
@@ -124,6 +133,9 @@ class AudioFile {
|
||||
this.ext = fileData.ext
|
||||
this.path = fileData.path
|
||||
this.fullPath = fileData.fullPath
|
||||
this.mtimeMs = fileData.mtimeMs || 0
|
||||
this.ctimeMs = fileData.ctimeMs || 0
|
||||
this.birthtimeMs = fileData.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
|
||||
this.trackNumFromMeta = fileData.trackNumFromMeta
|
||||
|
||||
+119
-25
@@ -1,10 +1,11 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const { bytesPretty, readTextFile } = require('../utils/fileUtils')
|
||||
const { comparePaths, getIno, getId, elapsedPretty } = require('../utils/index')
|
||||
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
|
||||
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
|
||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||
const nfoGenerator = require('../utils/nfoGenerator')
|
||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
||||
const Logger = require('../Logger')
|
||||
const Book = require('./Book')
|
||||
const AudioTrack = require('./AudioTrack')
|
||||
@@ -21,6 +22,9 @@ class Audiobook {
|
||||
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
this.addedAt = null
|
||||
this.lastUpdate = null
|
||||
this.lastScan = null
|
||||
@@ -44,6 +48,9 @@ class Audiobook {
|
||||
if (audiobook) {
|
||||
this.construct(audiobook)
|
||||
}
|
||||
|
||||
// Temp flags
|
||||
this.isSavingMetadata = false
|
||||
}
|
||||
|
||||
construct(audiobook) {
|
||||
@@ -53,6 +60,9 @@ class Audiobook {
|
||||
this.folderId = audiobook.folderId || 'audiobooks'
|
||||
this.path = audiobook.path
|
||||
this.fullPath = audiobook.fullPath
|
||||
this.mtimeMs = audiobook.mtimeMs || 0
|
||||
this.ctimeMs = audiobook.ctimeMs || 0
|
||||
this.birthtimeMs = audiobook.birthtimeMs || 0
|
||||
this.addedAt = audiobook.addedAt
|
||||
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
||||
this.lastScan = audiobook.lastScan || null
|
||||
@@ -175,6 +185,9 @@ class Audiobook {
|
||||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
lastScan: this.lastScan,
|
||||
@@ -201,6 +214,9 @@ class Audiobook {
|
||||
tags: this.tags,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
duration: this.duration,
|
||||
@@ -224,6 +240,9 @@ class Audiobook {
|
||||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
duration: this.duration,
|
||||
@@ -331,6 +350,9 @@ class Audiobook {
|
||||
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
this.lastUpdate = this.addedAt
|
||||
|
||||
@@ -422,13 +444,8 @@ class Audiobook {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (payload.book) {
|
||||
if (!this.book) {
|
||||
this.setBook(payload.book)
|
||||
hasUpdates = true
|
||||
} else if (this.book.update(payload.book)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
if (payload.book && this.book.update(payload.book)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
@@ -523,7 +540,7 @@ class Audiobook {
|
||||
}
|
||||
|
||||
// On scan check other files found with other files saved
|
||||
async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
|
||||
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
|
||||
var hasUpdates = false
|
||||
|
||||
var currOtherFileNum = this.otherFiles.length
|
||||
@@ -532,6 +549,8 @@ class Audiobook {
|
||||
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
|
||||
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
|
||||
|
||||
var existingAbMetadata = this.otherFiles.find(file => file.filename === 'metadata.abs')
|
||||
|
||||
// Filter out other files no longer in directory
|
||||
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
||||
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
||||
@@ -540,9 +559,9 @@ class Audiobook {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
|
||||
// If desc.txt is new then read it and update description (will overwrite)
|
||||
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
|
||||
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
|
||||
if (descriptionTxt && !alreadyHasDescTxt) {
|
||||
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
||||
if (newDescription) {
|
||||
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
|
||||
@@ -550,9 +569,9 @@ class Audiobook {
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
|
||||
// If reader.txt is new then read it and update narrator (will overwrite)
|
||||
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
|
||||
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
||||
if (readerTxt && !alreadyHasReaderTxt) {
|
||||
var newReader = await readTextFile(readerTxt.fullPath)
|
||||
if (newReader) {
|
||||
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
|
||||
@@ -561,7 +580,28 @@ class Audiobook {
|
||||
}
|
||||
}
|
||||
|
||||
// If OPF file and was not already there
|
||||
|
||||
// If metadata.abs is new OR modified then read it and set all defined keys (will overwrite)
|
||||
var metadataAbs = newOtherFiles.find(file => file.filename === 'metadata.abs')
|
||||
var shouldUpdateAbs = !!metadataAbs && (metadataAbs.modified || !existingAbMetadata)
|
||||
if (metadataAbs && metadataAbs.modified) {
|
||||
Logger.debug(`[Audiobook] metadata.abs file was modified for "${this.title}"`)
|
||||
}
|
||||
|
||||
if (shouldUpdateAbs) {
|
||||
var abmetadataText = await readTextFile(metadataAbs.fullPath)
|
||||
if (abmetadataText) {
|
||||
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
|
||||
if (metadataUpdateObject && metadataUpdateObject.book) {
|
||||
if (this.update(metadataUpdateObject)) {
|
||||
Logger.debug(`[Audiobook] Some details were updated from metadata.abs for "${this.title}"`, metadataUpdateObject)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If OPF file and was not already there OR prefer opf metadata
|
||||
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
|
||||
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
|
||||
var xmlText = await readTextFile(metadataOpf.fullPath)
|
||||
@@ -640,7 +680,7 @@ class Audiobook {
|
||||
if (bookCoverPath && bookCoverPath.startsWith('/metadata')) {
|
||||
// Fixing old cover paths
|
||||
if (!this.book.coverFullPath) {
|
||||
this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
|
||||
this.book.coverFullPath = Path.join(global.MetadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/')
|
||||
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
||||
hasUpdates = true
|
||||
}
|
||||
@@ -797,9 +837,10 @@ class Audiobook {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for desc.txt and reader.txt and update details if found
|
||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
|
||||
var bookUpdatePayload = {}
|
||||
|
||||
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
|
||||
if (descriptionText) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
|
||||
@@ -811,6 +852,22 @@ class Audiobook {
|
||||
bookUpdatePayload.narrator = readerText
|
||||
}
|
||||
|
||||
// abmetadata will always overwrite
|
||||
var abmetadataText = await this.fetchTextFromTextFile('metadata.abs')
|
||||
if (abmetadataText) {
|
||||
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
|
||||
if (metadataUpdateObject && metadataUpdateObject.book) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" found metadata.abs file`)
|
||||
for (const key in metadataUpdateObject.book) {
|
||||
var value = metadataUpdateObject.book[key]
|
||||
if (key && value !== undefined) {
|
||||
bookUpdatePayload[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opf only overwrites if detail is empty
|
||||
var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
|
||||
if (metadataOpf) {
|
||||
var xmlText = await readTextFile(metadataOpf.fullPath)
|
||||
@@ -870,12 +927,6 @@ class Audiobook {
|
||||
}
|
||||
}
|
||||
|
||||
if (existingFile.filename !== fileFound.filename) {
|
||||
existingFile.filename = fileFound.filename
|
||||
existingFile.ext = fileFound.ext
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
if (existingFile.path !== fileFound.path) {
|
||||
existingFile.path = fileFound.path
|
||||
existingFile.fullPath = fileFound.fullPath
|
||||
@@ -885,6 +936,20 @@ class Audiobook {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
|
||||
keysToCheck.forEach((key) => {
|
||||
if (existingFile[key] !== fileFound[key]) {
|
||||
|
||||
// Add modified flag on file data object if exists and was changed
|
||||
if (key === 'mtimeMs' && existingFile[key]) {
|
||||
fileFound.modified = true
|
||||
}
|
||||
|
||||
existingFile[key] = fileFound[key]
|
||||
hasUpdated = true
|
||||
}
|
||||
})
|
||||
|
||||
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
|
||||
existingFile.filetype = fileFound.filetype
|
||||
hasUpdated = true
|
||||
@@ -924,6 +989,14 @@ class Audiobook {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
|
||||
keysToCheck.forEach((key) => {
|
||||
if (dataFound[key] != this[key]) {
|
||||
this[key] = dataFound[key] || 0
|
||||
hasUpdated = true
|
||||
}
|
||||
})
|
||||
|
||||
var newAudioFileData = []
|
||||
var newOtherFileData = []
|
||||
var existingAudioFileData = []
|
||||
@@ -1014,14 +1087,14 @@ class Audiobook {
|
||||
}
|
||||
|
||||
// Temp fix for cover is set but coverFullPath is not set
|
||||
fixFullCoverPath(metadataPath) {
|
||||
fixFullCoverPath() {
|
||||
if (!this.book.cover) return
|
||||
var bookCoverPath = this.book.cover.replace(/\\/g, '/')
|
||||
var newFullCoverPath = null
|
||||
if (bookCoverPath.startsWith('/s/book/')) {
|
||||
newFullCoverPath = Path.join(this.fullPath, bookCoverPath.substr(`/s/book/${this.id}`.length)).replace(/\/\//g, '/')
|
||||
} else if (bookCoverPath.startsWith('/metadata/')) {
|
||||
newFullCoverPath = Path.join(metadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
|
||||
newFullCoverPath = Path.join(global.MetadataPath, bookCoverPath.substr('/metadata/'.length)).replace(/\/\//g, '/')
|
||||
}
|
||||
if (newFullCoverPath) {
|
||||
Logger.debug(`[Audiobook] "${this.title}" fixing full cover path "${this.book.cover}" => "${newFullCoverPath}"`)
|
||||
@@ -1030,5 +1103,26 @@ class Audiobook {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async saveAbMetadata() {
|
||||
if (this.isSavingMetadata) return
|
||||
this.isSavingMetadata = true
|
||||
|
||||
var metadataPath = Path.join(global.MetadataPath, 'books', this.id)
|
||||
if (global.ServerSettings.storeMetadataWithBook) {
|
||||
metadataPath = this.fullPath
|
||||
} else {
|
||||
// Make sure metadata book dir exists
|
||||
await fs.ensureDir(metadataPath)
|
||||
}
|
||||
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
||||
|
||||
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
||||
this.isSavingMetadata = false
|
||||
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
|
||||
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
|
||||
return success
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = Audiobook
|
||||
@@ -6,6 +6,11 @@ class AudiobookFile {
|
||||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.size = null
|
||||
this.mtimeMs = null
|
||||
this.ctimeMs = null
|
||||
this.birthtimeMs = null
|
||||
|
||||
this.addedAt = null
|
||||
|
||||
if (data) {
|
||||
@@ -25,6 +30,10 @@ class AudiobookFile {
|
||||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
size: this.size,
|
||||
mtimeMs: this.mtimeMs,
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt
|
||||
}
|
||||
}
|
||||
@@ -36,6 +45,10 @@ class AudiobookFile {
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.size = data.size || 0
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = data.addedAt
|
||||
}
|
||||
|
||||
@@ -46,6 +59,10 @@ class AudiobookFile {
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.size = data.size || 0
|
||||
this.mtimeMs = data.mtimeMs || 0
|
||||
this.ctimeMs = data.ctimeMs || 0
|
||||
this.birthtimeMs = data.birthtimeMs || 0
|
||||
this.addedAt = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { CoverDestination, BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
|
||||
const { BookCoverAspectRatio, BookshelfView } = require('../utils/constants')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class ServerSettings {
|
||||
@@ -18,8 +18,8 @@ class ServerSettings {
|
||||
this.scannerDisableWatcher = false
|
||||
|
||||
// Metadata
|
||||
this.coverDestination = CoverDestination.METADATA
|
||||
this.saveMetadataFile = false
|
||||
this.storeCoverWithBook = false
|
||||
this.storeMetadataWithBook = false
|
||||
|
||||
// Security/Rate limits
|
||||
this.rateLimitLoginRequests = 10
|
||||
@@ -59,8 +59,12 @@ class ServerSettings {
|
||||
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
|
||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||
|
||||
this.coverDestination = settings.coverDestination || CoverDestination.METADATA
|
||||
this.saveMetadataFile = !!settings.saveMetadataFile
|
||||
this.storeCoverWithBook = settings.storeCoverWithBook
|
||||
if (this.storeCoverWithBook == undefined) { // storeCoverWithBook added in 1.7.1 to replace coverDestination
|
||||
this.storeCoverWithBook = !!settings.coverDestination
|
||||
}
|
||||
this.storeMetadataWithBook = !!settings.storeCoverWithBook
|
||||
|
||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||
|
||||
@@ -95,8 +99,8 @@ class ServerSettings {
|
||||
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
|
||||
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
|
||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||
coverDestination: this.coverDestination,
|
||||
saveMetadataFile: !!this.saveMetadataFile,
|
||||
storeCoverWithBook: this.storeCoverWithBook,
|
||||
storeMetadataWithBook: this.storeMetadataWithBook,
|
||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||
backupSchedule: this.backupSchedule,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const AuthorFinder = require('../AuthorFinder')
|
||||
|
||||
class AuthorScanner {
|
||||
constructor(db, MetadataPath) {
|
||||
constructor(db) {
|
||||
this.db = db
|
||||
this.MetadataPath = MetadataPath
|
||||
this.authorFinder = new AuthorFinder(MetadataPath)
|
||||
this.authorFinder = new AuthorFinder()
|
||||
}
|
||||
|
||||
getUniqueAuthors() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const { CoverDestination } = require('../utils/constants')
|
||||
|
||||
class ScanOptions {
|
||||
constructor(options) {
|
||||
this.forceRescan = false
|
||||
@@ -7,7 +5,7 @@ class ScanOptions {
|
||||
// Server settings
|
||||
this.parseSubtitles = false
|
||||
this.findCovers = false
|
||||
this.coverDestination = CoverDestination.METADATA
|
||||
this.storeCoverWithBook = false
|
||||
this.preferAudioMetadata = false
|
||||
this.preferOpfMetadata = false
|
||||
|
||||
@@ -32,7 +30,7 @@ class ScanOptions {
|
||||
metadataPrecedence: this.metadataPrecedence,
|
||||
parseSubtitles: this.parseSubtitles,
|
||||
findCovers: this.findCovers,
|
||||
coverDestination: this.coverDestination,
|
||||
storeCoverWithBook: this.storeCoverWithBook,
|
||||
preferAudioMetadata: this.preferAudioMetadata,
|
||||
preferOpfMetadata: this.preferOpfMetadata
|
||||
}
|
||||
@@ -43,7 +41,7 @@ class ScanOptions {
|
||||
|
||||
this.parseSubtitles = !!serverSettings.scannerParseSubtitle
|
||||
this.findCovers = !!serverSettings.scannerFindCovers
|
||||
this.coverDestination = serverSettings.coverDestination
|
||||
this.storeCoverWithBook = serverSettings.storeCoverWithBook
|
||||
this.preferAudioMetadata = serverSettings.scannerPreferAudioMetadata
|
||||
this.preferOpfMetadata = serverSettings.scannerPreferOpfMetadata
|
||||
}
|
||||
|
||||
+15
-17
@@ -5,8 +5,8 @@ const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const { version } = require('../../package.json')
|
||||
const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir')
|
||||
const { comparePaths, getIno, getId, msToTimestamp } = require('../utils/index')
|
||||
const { ScanResult, CoverDestination, LogLevel } = require('../utils/constants')
|
||||
const { comparePaths, getId } = require('../utils/index')
|
||||
const { ScanResult, LogLevel } = require('../utils/constants')
|
||||
|
||||
const AudioFileScanner = require('./AudioFileScanner')
|
||||
const BookFinder = require('../BookFinder')
|
||||
@@ -15,12 +15,9 @@ const LibraryScan = require('./LibraryScan')
|
||||
const ScanOptions = require('./ScanOptions')
|
||||
|
||||
class Scanner {
|
||||
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) {
|
||||
this.AudiobookPath = AUDIOBOOK_PATH
|
||||
this.MetadataPath = METADATA_PATH
|
||||
this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books')
|
||||
var LogDirPath = Path.join(this.MetadataPath, 'logs')
|
||||
this.ScanLogPath = Path.join(LogDirPath, 'scans')
|
||||
constructor(db, coverController, emitter) {
|
||||
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
|
||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||
|
||||
this.db = db
|
||||
this.coverController = coverController
|
||||
@@ -33,7 +30,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
getCoverDirectory(audiobook) {
|
||||
if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
|
||||
if (this.db.serverSettings.storeCoverWithBook) {
|
||||
return {
|
||||
fullPath: audiobook.fullPath,
|
||||
relPath: '/s/book/' + audiobook.id
|
||||
@@ -88,8 +85,8 @@ class Scanner {
|
||||
|
||||
// Sync other files first so that local images are used as cover art
|
||||
// TODO: Cleanup other file sync
|
||||
var allOtherFiles = checkRes.newOtherFileData.concat(audiobook._otherFiles)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, this.db.serverSettings.scannerPreferOpfMetadata)) {
|
||||
var allOtherFiles = checkRes.newOtherFileData.concat(checkRes.existingOtherFileData)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.db.serverSettings.scannerPreferOpfMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
||||
@@ -120,7 +117,7 @@ class Scanner {
|
||||
|
||||
if (hasUpdated) {
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
await this.db.updateEntity('audiobook', audiobook)
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
return ScanResult.UPDATED
|
||||
}
|
||||
return ScanResult.UPTODATE
|
||||
@@ -208,6 +205,7 @@ class Scanner {
|
||||
// Check for existing & removed audiobooks
|
||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||
var audiobook = audiobooksInLibrary[i]
|
||||
// Find audiobook folder with matching inode or matching path
|
||||
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
|
||||
if (!dataFound) {
|
||||
libraryScan.addLog(LogLevel.WARN, `Audiobook "${audiobook.title}" is missing`)
|
||||
@@ -317,7 +315,7 @@ class Scanner {
|
||||
}))
|
||||
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
|
||||
libraryScan.resultsAdded += newAudiobooks.length
|
||||
await this.db.insertEntities('audiobook', newAudiobooks)
|
||||
await this.db.insertAudiobooks(newAudiobooks)
|
||||
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
|
||||
}
|
||||
|
||||
@@ -330,7 +328,7 @@ class Scanner {
|
||||
if (newOtherFileData.length || libraryScan.scanOptions.forceRescan) {
|
||||
// TODO: Cleanup other file sync
|
||||
var allOtherFiles = newOtherFileData.concat(existingOtherFileData)
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, this.MetadataPath, libraryScan.preferOpfMetadata)) {
|
||||
if (await audiobook.syncOtherFiles(allOtherFiles, libraryScan.preferOpfMetadata)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
@@ -525,7 +523,7 @@ class Scanner {
|
||||
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
|
||||
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
|
||||
if (newAudiobook) {
|
||||
await this.db.insertEntity('audiobook', newAudiobook)
|
||||
await this.db.insertAudiobook(newAudiobook)
|
||||
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
|
||||
}
|
||||
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
|
||||
@@ -615,7 +613,7 @@ class Scanner {
|
||||
}
|
||||
Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id)
|
||||
await this.db.removeEntity('audiobook', ab.id)
|
||||
await this.db.insertEntity('audiobook', abCopy)
|
||||
await this.db.insertAudiobook(abCopy)
|
||||
audiobooksUpdated++
|
||||
} else {
|
||||
ids[ab.id] = true
|
||||
@@ -668,7 +666,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('audiobook', audiobook)
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
const fs = require('fs-extra')
|
||||
const filePerms = require('./filePerms')
|
||||
const package = require('../../package.json')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const bookKeyMap = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
author: 'authorFL',
|
||||
narrator: 'narratorFL',
|
||||
series: 'series',
|
||||
volumeNumber: 'volumeNumber',
|
||||
publishYear: 'publishYear',
|
||||
publisher: 'publisher',
|
||||
description: 'description',
|
||||
isbn: 'isbn',
|
||||
asin: 'asin',
|
||||
language: 'language',
|
||||
genres: 'genresCommaSeparated'
|
||||
}
|
||||
|
||||
function generate(audiobook, outputPath) {
|
||||
var fileString = ';ABMETADATA1\n'
|
||||
fileString += `#audiobookshelf v${package.version}\n\n`
|
||||
|
||||
for (const key in bookKeyMap) {
|
||||
const value = audiobook.book[bookKeyMap[key]] || ''
|
||||
fileString += `${key}=${value}\n`
|
||||
}
|
||||
|
||||
if (audiobook.chapters.length) {
|
||||
fileString += '\n'
|
||||
audiobook.chapters.forEach((chapter) => {
|
||||
fileString += `[CHAPTER]\n`
|
||||
fileString += `start=${chapter.start}\n`
|
||||
fileString += `end=${chapter.end}\n`
|
||||
fileString += `title=${chapter.title}\n`
|
||||
})
|
||||
}
|
||||
|
||||
return fs.writeFile(outputPath, fileString).then(() => {
|
||||
return filePerms(outputPath, 0o774, global.Uid, global.Gid, true).then((data) => true)
|
||||
}).catch((error) => {
|
||||
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
module.exports.generate = generate
|
||||
|
||||
function parseAbMetadataText(text) {
|
||||
if (!text) return null
|
||||
var lines = text.split(/\r?\n/)
|
||||
|
||||
// Check first line and get abmetadata version number
|
||||
var firstLine = lines.shift().toLowerCase()
|
||||
if (!firstLine.startsWith(';abmetadata')) {
|
||||
Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`)
|
||||
return null
|
||||
}
|
||||
var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim())
|
||||
if (isNaN(abmetadataVersion)) {
|
||||
Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - using 1`)
|
||||
abmetadataVersion = 1
|
||||
}
|
||||
|
||||
// Remove comments and empty lines
|
||||
const ignoreFirstChars = [' ', '#', ';'] // Ignore any line starting with the following
|
||||
lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0]))
|
||||
|
||||
// Get lines that map to book details (all lines before the first chapter section)
|
||||
var firstSectionLine = lines.findIndex(l => l.startsWith('['))
|
||||
var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines
|
||||
|
||||
// Put valid book detail values into map
|
||||
const bookDetails = {}
|
||||
for (let i = 0; i < detailLines.length; i++) {
|
||||
var line = detailLines[i]
|
||||
var keyValue = line.split('=')
|
||||
if (keyValue.length < 2) {
|
||||
Logger.warn('abmetadata invalid line has no =', line)
|
||||
} else if (!bookKeyMap[keyValue[0].trim()]) {
|
||||
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid book detail key`)
|
||||
} else {
|
||||
var key = keyValue[0].trim()
|
||||
bookDetails[key] = keyValue[1].trim()
|
||||
|
||||
// Genres convert to array of strings
|
||||
if (key === 'genres') {
|
||||
bookDetails[key] = bookDetails[key] ? bookDetails[key].split(',').map(genre => genre.trim()) : []
|
||||
} else if (!bookDetails[key]) { // Use null for empty details
|
||||
bookDetails[key] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Chapter support
|
||||
|
||||
return {
|
||||
book: bookDetails
|
||||
}
|
||||
}
|
||||
module.exports.parse = parseAbMetadataText
|
||||
@@ -1,37 +0,0 @@
|
||||
const fs = require('fs-extra')
|
||||
const filePerms = require('./filePerms')
|
||||
const package = require('../../package.json')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const bookKeyMap = {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
author: 'authorFL',
|
||||
narrator: 'narratorFL',
|
||||
series: 'series',
|
||||
volumeNumber: 'volumeNumber',
|
||||
publishYear: 'publishYear',
|
||||
publisher: 'publisher',
|
||||
description: 'description',
|
||||
isbn: 'isbn',
|
||||
asin: 'asin',
|
||||
language: 'language',
|
||||
genres: 'genresCommaSeparated'
|
||||
}
|
||||
|
||||
function generate(audiobook, outputPath, uid, gid) {
|
||||
var fileString = `[audiobookshelf v${package.version}]\n`
|
||||
|
||||
for (const key in bookKeyMap) {
|
||||
const value = audiobook.book[bookKeyMap[key]] || ''
|
||||
fileString += `${key}=${value}\n`
|
||||
}
|
||||
|
||||
return fs.writeFile(outputPath, fileString).then(() => {
|
||||
return filePerms(outputPath, 0o774, uid, gid).then(() => true)
|
||||
}).catch((error) => {
|
||||
Logger.error(`[absMetaFileGenerator] Failed to save abs file`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
module.exports.generate = generate
|
||||
@@ -6,11 +6,6 @@ module.exports.ScanResult = {
|
||||
UPTODATE: 4
|
||||
}
|
||||
|
||||
module.exports.CoverDestination = {
|
||||
METADATA: 0,
|
||||
AUDIOBOOK: 1
|
||||
}
|
||||
|
||||
module.exports.BookCoverAspectRatio = {
|
||||
STANDARD: 0, // 1.6:1
|
||||
SQUARE: 1
|
||||
|
||||
@@ -50,7 +50,7 @@ const chmodr = (p, mode, uid, gid, cb) => {
|
||||
// any error other than ENOTDIR means it's not readable, or
|
||||
// doesn't exist. give up.
|
||||
if (er && er.code !== 'ENOTDIR') return cb(er)
|
||||
if (er) {
|
||||
if (er) { // Is a file
|
||||
return fs.chmod(p, mode).then(() => {
|
||||
fs.chown(p, uid, gid, cb)
|
||||
})
|
||||
@@ -77,9 +77,9 @@ const chmodr = (p, mode, uid, gid, cb) => {
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (path, mode, uid, gid) => {
|
||||
module.exports = (path, mode, uid, gid, silent = false) => {
|
||||
return new Promise((resolve) => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,23 @@ async function getFileStat(path) {
|
||||
}
|
||||
module.exports.getFileStat = getFileStat
|
||||
|
||||
async function getFileTimestampsWithIno(path) {
|
||||
try {
|
||||
var stat = await fs.stat(path, { bigint: true })
|
||||
return {
|
||||
size: Number(stat.size),
|
||||
mtimeMs: Number(stat.mtimeMs),
|
||||
ctimeMs: Number(stat.ctimeMs),
|
||||
birthtimeMs: Number(stat.birthtimeMs),
|
||||
ino: String(stat.ino)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to getFileTimestampsWithIno', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
|
||||
|
||||
async function getFileSize(path) {
|
||||
var stat = await getFileStat(path)
|
||||
if (!stat) return 0
|
||||
@@ -27,6 +44,15 @@ async function getFileSize(path) {
|
||||
}
|
||||
module.exports.getFileSize = getFileSize
|
||||
|
||||
|
||||
function getIno(path) {
|
||||
return fs.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
}
|
||||
module.exports.getIno = getIno
|
||||
|
||||
async function readTextFile(path) {
|
||||
try {
|
||||
var data = await fs.readFile(path)
|
||||
|
||||
@@ -38,13 +38,6 @@ module.exports.comparePaths = (path1, path2) => {
|
||||
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
|
||||
}
|
||||
|
||||
module.exports.getIno = (path) => {
|
||||
return fs.promises.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.isNullOrNaN = (num) => {
|
||||
return num === null || isNaN(num)
|
||||
}
|
||||
|
||||
+48
-31
@@ -1,8 +1,7 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const Logger = require('../Logger')
|
||||
const { getIno } = require('./index')
|
||||
const { recurseFiles } = require('./fileUtils')
|
||||
const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils')
|
||||
const globals = require('./globals')
|
||||
|
||||
function isBookFile(path) {
|
||||
@@ -114,16 +113,20 @@ function groupFileItemsIntoBooks(fileItems) {
|
||||
}
|
||||
|
||||
function cleanFileObjects(basepath, abrelpath, files) {
|
||||
return files.map((file) => {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
var fullPath = Path.posix.join(basepath, file)
|
||||
var fileTsData = await getFileTimestampsWithIno(fullPath)
|
||||
|
||||
var ext = Path.extname(file)
|
||||
return {
|
||||
filetype: getFileType(ext),
|
||||
filename: Path.basename(file),
|
||||
path: Path.posix.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3
|
||||
fullPath: Path.posix.join(basepath, file), // /audiobooks/AUDIOBOOK/PATH/filename.mp3
|
||||
ext: ext
|
||||
fullPath, // /audiobooks/AUDIOBOOK/PATH/filename.mp3
|
||||
ext: ext,
|
||||
...fileTsData
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function getFileType(ext) {
|
||||
@@ -162,15 +165,15 @@ async function scanRootDir(folder, serverSettings = {}) {
|
||||
for (const audiobookPath in audiobookGrouping) {
|
||||
var audiobookData = getAudiobookDataFromDir(folderPath, audiobookPath, parseSubtitle)
|
||||
|
||||
var fileObjs = cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
|
||||
for (let i = 0; i < fileObjs.length; i++) {
|
||||
fileObjs[i].ino = await getIno(fileObjs[i].fullPath)
|
||||
}
|
||||
var audiobookIno = await getIno(audiobookData.fullPath)
|
||||
var fileObjs = await cleanFileObjects(audiobookData.fullPath, audiobookPath, audiobookGrouping[audiobookPath])
|
||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
||||
audiobooks.push({
|
||||
folderId: folder.id,
|
||||
libraryId: folder.libraryId,
|
||||
ino: audiobookIno,
|
||||
ino: audiobookFolderStats.ino,
|
||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
||||
...audiobookData,
|
||||
audioFiles: fileObjs.filter(f => f.filetype === 'audio'),
|
||||
otherFiles: fileObjs.filter(f => f.filetype !== 'audio')
|
||||
@@ -196,32 +199,42 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) {
|
||||
|
||||
|
||||
// If in a series directory check for volume number match
|
||||
/* ACCEPTS:
|
||||
/* ACCEPTS
|
||||
Book 2 - Title Here - Subtitle Here
|
||||
Title Here - Subtitle Here - Vol 12
|
||||
Title Here - volume 9 - Subtitle Here
|
||||
Vol. 3 Title Here - Subtitle Here
|
||||
1980 - Book 2-Title Here
|
||||
Title Here-Volume 999-Subtitle Here
|
||||
2 - Book Title
|
||||
100 - Book Title
|
||||
0.5 - Book Title
|
||||
*/
|
||||
var volumeNumber = null
|
||||
if (series) {
|
||||
// New volume regex to match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
|
||||
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
|
||||
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
|
||||
volumeNumber = volumeMatch[3]
|
||||
var replaceChunk = volumeMatch[2]
|
||||
// Added 1.7.1: If title starts with a # that is 3 digits or less (or w/ 2 decimal), then use as volume number
|
||||
var volumeMatch = title.match(/^(\d{1,3}(?:\.\d{1,2})?) - ./)
|
||||
if (volumeMatch && volumeMatch.length > 1) {
|
||||
volumeNumber = volumeMatch[1]
|
||||
title = title.replace(`${volumeNumber} - `, '')
|
||||
} else {
|
||||
// Match volumes with decimal (OLD: /(-? ?)\b((?:Book|Vol.?|Volume) (\d{1,3}))\b( ?-?)/i)
|
||||
var volumeMatch = title.match(/(-? ?)\b((?:Book|Vol.?|Volume) (\d{0,3}(?:\.\d{1,2})?))\b( ?-?)/i)
|
||||
if (volumeMatch && volumeMatch.length > 3 && volumeMatch[2] && volumeMatch[3]) {
|
||||
volumeNumber = volumeMatch[3]
|
||||
var replaceChunk = volumeMatch[2]
|
||||
|
||||
// "1980 - Book 2-Title Here"
|
||||
// Group 1 would be "- "
|
||||
// Group 3 would be "-"
|
||||
// Only remove the first group
|
||||
if (volumeMatch[1]) {
|
||||
replaceChunk = volumeMatch[1] + replaceChunk
|
||||
} else if (volumeMatch[4]) {
|
||||
replaceChunk += volumeMatch[4]
|
||||
// "1980 - Book 2-Title Here"
|
||||
// Group 1 would be "- "
|
||||
// Group 3 would be "-"
|
||||
// Only remove the first group
|
||||
if (volumeMatch[1]) {
|
||||
replaceChunk = volumeMatch[1] + replaceChunk
|
||||
} else if (volumeMatch[4]) {
|
||||
replaceChunk += volumeMatch[4]
|
||||
}
|
||||
title = title.replace(replaceChunk, '').trim()
|
||||
}
|
||||
title = title.replace(replaceChunk, '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +285,12 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
|
||||
|
||||
var audiobookDir = audiobookPath.replace(folderFullPath, '').slice(1)
|
||||
var audiobookData = getAudiobookDataFromDir(folderFullPath, audiobookDir, parseSubtitle)
|
||||
var audiobookFolderStats = await getFileTimestampsWithIno(audiobookData.fullPath)
|
||||
var audiobook = {
|
||||
ino: await getIno(audiobookData.fullPath),
|
||||
ino: audiobookFolderStats.ino,
|
||||
mtimeMs: audiobookFolderStats.mtimeMs || 0,
|
||||
ctimeMs: audiobookFolderStats.ctimeMs || 0,
|
||||
birthtimeMs: audiobookFolderStats.birthtimeMs || 0,
|
||||
folderId: folder.id,
|
||||
libraryId: folder.libraryId,
|
||||
...audiobookData,
|
||||
@@ -284,14 +301,14 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
|
||||
for (let i = 0; i < fileItems.length; i++) {
|
||||
var fileItem = fileItems[i]
|
||||
|
||||
var ino = await getIno(fileItem.fullpath)
|
||||
var fileStatData = await getFileTimestampsWithIno(fileItem.fullpath)
|
||||
var fileObj = {
|
||||
ino,
|
||||
filetype: getFileType(fileItem.extension),
|
||||
filename: fileItem.name,
|
||||
path: fileItem.path,
|
||||
fullPath: fileItem.fullpath,
|
||||
ext: fileItem.extension
|
||||
ext: fileItem.extension,
|
||||
...fileStatData
|
||||
}
|
||||
if (fileObj.filetype === 'audio') {
|
||||
audiobook.audioFiles.push(fileObj)
|
||||
|
||||
Reference in New Issue
Block a user