mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Adding inode to files and audiobooks to support renaming, setting up watcher and removing chokidar
This commit is contained in:
parent
81487d1dba
commit
63cae5b0ed
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "0.9.75-beta",
|
||||
"version": "0.9.77-beta",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -4,7 +4,8 @@ module.exports = {
|
||||
purge: {
|
||||
options: {
|
||||
safelist: [
|
||||
'bg-success'
|
||||
'bg-success',
|
||||
'bg-red-600'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
166
package-lock.json
generated
166
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "0.9.72-beta",
|
||||
"version": "0.9.76-beta",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -69,6 +69,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"aborter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aborter/-/aborter-1.1.0.tgz",
|
||||
"integrity": "sha512-9rHWMcWTEYsMB4l+ttgPujR7OiXH9NQbP0ej+SSVaK1e2yU/tePbYm8g/g9cQhJkgczp6lpEB2fdJYLKT/T0mg=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@ -78,13 +83,12 @@
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"are-shallow-equal": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/are-shallow-equal/-/are-shallow-equal-1.1.1.tgz",
|
||||
"integrity": "sha512-Y0MC/7IP+WZSo0NgYDwww7euKssEodUJxjby3fmNurEDcbq8htqSgyI7a7HELJzkzNv26dOH5vKQFlzCt1H9Ag==",
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
"is-primitive": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
@ -97,6 +101,11 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
|
||||
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
|
||||
},
|
||||
"atomically": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
|
||||
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
@ -125,11 +134,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
@ -156,14 +160,6 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@ -193,21 +189,6 @@
|
||||
"responselike": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
@ -267,6 +248,11 @@
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@ -420,14 +406,6 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||
@ -476,12 +454,6 @@
|
||||
"universalify": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
@ -490,14 +462,6 @@
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz",
|
||||
@ -571,31 +535,10 @@
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
"is-primitive": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
|
||||
"integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
@ -787,11 +730,6 @@
|
||||
"minimatch": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||
},
|
||||
"normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
@ -833,11 +771,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="
|
||||
},
|
||||
"podcast": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/podcast/-/podcast-1.3.0.tgz",
|
||||
@ -846,6 +779,11 @@
|
||||
"rss": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"promise-concurrency-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-concurrency-limiter/-/promise-concurrency-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-OI96yL5DUck9KCLee5H6DnRfVsHIstQspXk8xsYrWr9ur9IlFuzKvoU70HwQb99MqHg2mpdkuGa92NuoXue3cw=="
|
||||
},
|
||||
"proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
@ -900,14 +838,6 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"resolve-alpn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
|
||||
@ -926,6 +856,14 @@
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"ripstat": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ripstat/-/ripstat-1.1.1.tgz",
|
||||
"integrity": "sha512-O+KrJUwY3Q8cArNraH136svsDlNmRh6mnJ9TogkpcGWBvd2Kks5d5HGsZRnWt9h3kh8D4uq62kdlYihONjgj5w==",
|
||||
"requires": {
|
||||
"atomically": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"rss": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
|
||||
@ -1079,12 +1017,17 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"string-indexes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-indexes/-/string-indexes-1.0.0.tgz",
|
||||
"integrity": "sha512-RUlx+2YydZJNlRAvoh1siPYWj/Xfk6t1sQLkA5n1tMGRCKkRLzkRtJhHk4qRmKergEBh8R3pWhsUsDqia/bolw=="
|
||||
},
|
||||
"tiny-readdir": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-1.5.0.tgz",
|
||||
"integrity": "sha512-Nep9qu34bOZApNkEnJu4V1WcgxW1kCGlYN8SYwMzZfqpv6f2E1n5vPHLczJqy2vOQ1rQG/m9fI3DQbIFXAQNGw==",
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
"promise-concurrency-limiter": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"toidentifier": {
|
||||
@ -1121,6 +1064,19 @@
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"watcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/watcher/-/watcher-1.2.0.tgz",
|
||||
"integrity": "sha512-f2KFU8ZRSDfTXdI2Y6Ge7DXVnCx9QnlGScwUHPh+YPkbFWZS983KfgHddj+KBWZGrCCuRanYvDz91p65d9/h4w==",
|
||||
"requires": {
|
||||
"aborter": "^1.0.0",
|
||||
"are-shallow-equal": "^1.1.1",
|
||||
"debounce": "^1.2.0",
|
||||
"ripstat": "^1.1.1",
|
||||
"string-indexes": "^1.0.0",
|
||||
"tiny-readdir": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "0.9.75-beta",
|
||||
"version": "0.9.77-beta",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chokidar": "^3.5.2",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"express": "^4.17.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@ -23,7 +22,8 @@
|
||||
"njodb": "^0.4.20",
|
||||
"node-dir": "^0.1.17",
|
||||
"podcast": "^1.3.0",
|
||||
"socket.io": "^4.1.3"
|
||||
"socket.io": "^4.1.3",
|
||||
"watcher": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
135
server/AudioFile.js
Normal file
135
server/AudioFile.js
Normal file
@ -0,0 +1,135 @@
|
||||
class AudioFile {
|
||||
constructor(data) {
|
||||
this.index = null
|
||||
this.ino = null
|
||||
this.filename = null
|
||||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.addedAt = null
|
||||
|
||||
this.format = null
|
||||
this.duration = null
|
||||
this.size = null
|
||||
this.bitRate = null
|
||||
this.language = null
|
||||
this.codec = null
|
||||
this.timeBase = null
|
||||
this.channels = null
|
||||
this.channelLayout = null
|
||||
|
||||
this.tagAlbum = null
|
||||
this.tagArtist = null
|
||||
this.tagGenre = null
|
||||
this.tagTitle = null
|
||||
this.tagTrack = null
|
||||
|
||||
this.manuallyVerified = false
|
||||
this.invalid = false
|
||||
this.error = null
|
||||
|
||||
if (data) {
|
||||
this.construct(data)
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
index: this.index,
|
||||
ino: this.ino,
|
||||
filename: this.filename,
|
||||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
addedAt: this.addedAt,
|
||||
manuallyVerified: !!this.manuallyVerified,
|
||||
invalid: !!this.invalid,
|
||||
error: this.error || null,
|
||||
format: this.format,
|
||||
duration: this.duration,
|
||||
size: this.size,
|
||||
bitRate: this.bitRate,
|
||||
language: this.language,
|
||||
timeBase: this.timeBase,
|
||||
channels: this.channels,
|
||||
channelLayout: this.channelLayout,
|
||||
tagAlbum: this.tagAlbum,
|
||||
tagArtist: this.tagArtist,
|
||||
tagGenre: this.tagGenre,
|
||||
tagTitle: this.tagTitle,
|
||||
tagTrack: this.tagTrack
|
||||
}
|
||||
}
|
||||
|
||||
construct(data) {
|
||||
this.index = data.index
|
||||
this.ino = data.ino
|
||||
this.filename = data.filename
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.addedAt = data.addedAt
|
||||
this.manuallyVerified = !!data.manuallyVerified
|
||||
this.invalid = !!data.invalid
|
||||
this.error = data.error || null
|
||||
|
||||
this.format = data.format
|
||||
this.duration = data.duration
|
||||
this.size = data.size
|
||||
this.bitRate = data.bitRate
|
||||
this.language = data.language
|
||||
this.codec = data.codec
|
||||
this.timeBase = data.timeBase
|
||||
this.channels = data.channels
|
||||
this.channelLayout = data.channelLayout
|
||||
|
||||
this.tagAlbum = data.tagAlbum
|
||||
this.tagArtist = data.tagArtist
|
||||
this.tagGenre = data.tagGenre
|
||||
this.tagTitle = data.tagTitle
|
||||
this.tagTrack = data.tagTrack
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.index = data.index || null
|
||||
this.ino = data.ino
|
||||
this.filename = data.filename
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.addedAt = Date.now()
|
||||
|
||||
this.manuallyVerified = !!data.manuallyVerified
|
||||
this.invalid = !!data.invalid
|
||||
this.error = data.error || null
|
||||
|
||||
this.format = data.format
|
||||
this.duration = data.duration
|
||||
this.size = data.size
|
||||
this.bitRate = data.bit_rate
|
||||
this.language = data.language
|
||||
this.codec = data.codec
|
||||
this.timeBase = data.time_base
|
||||
this.channels = data.channels
|
||||
this.channelLayout = data.channel_layout
|
||||
|
||||
this.tagAlbum = data.file_tag_album || null
|
||||
this.tagArtist = data.file_tag_artist || null
|
||||
this.tagGenre = data.file_tag_genre || null
|
||||
this.tagTitle = data.file_tag_title || null
|
||||
this.tagTrack = data.file_tag_track || null
|
||||
}
|
||||
|
||||
syncFile(newFile) {
|
||||
var hasUpdates = false
|
||||
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
|
||||
keysToSync.forEach((key) => {
|
||||
if (newFile[key] !== undefined && newFile[key] !== this[key]) {
|
||||
hasUpdates = true
|
||||
this[key] = newFile[key]
|
||||
}
|
||||
})
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = AudioFile
|
@ -3,6 +3,8 @@ var { bytesPretty } = require('./utils/fileUtils')
|
||||
class AudioTrack {
|
||||
constructor(audioTrack = null) {
|
||||
this.index = null
|
||||
this.ino = null
|
||||
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.ext = null
|
||||
@ -31,6 +33,8 @@ class AudioTrack {
|
||||
|
||||
construct(audioTrack) {
|
||||
this.index = audioTrack.index
|
||||
this.ino = audioTrack.ino || null
|
||||
|
||||
this.path = audioTrack.path
|
||||
this.fullPath = audioTrack.fullPath
|
||||
this.ext = audioTrack.ext
|
||||
@ -45,6 +49,12 @@ class AudioTrack {
|
||||
this.timeBase = audioTrack.timeBase
|
||||
this.channels = audioTrack.channels
|
||||
this.channelLayout = audioTrack.channelLayout
|
||||
|
||||
this.tagAlbum = audioTrack.tagAlbum
|
||||
this.tagArtist = audioTrack.tagArtist
|
||||
this.tagGenre = audioTrack.tagGenre
|
||||
this.tagTitle = audioTrack.tagTitle
|
||||
this.tagTrack = audioTrack.tagTrack
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -54,6 +64,7 @@ class AudioTrack {
|
||||
toJSON() {
|
||||
return {
|
||||
index: this.index,
|
||||
ino: this.ino,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
ext: this.ext,
|
||||
@ -65,12 +76,19 @@ class AudioTrack {
|
||||
language: this.language,
|
||||
timeBase: this.timeBase,
|
||||
channels: this.channels,
|
||||
channelLayout: this.channelLayout
|
||||
channelLayout: this.channelLayout,
|
||||
tagAlbum: this.tagAlbum,
|
||||
tagArtist: this.tagArtist,
|
||||
tagGenre: this.tagGenre,
|
||||
tagTitle: this.tagTitle,
|
||||
tagTrack: this.tagTrack
|
||||
}
|
||||
}
|
||||
|
||||
setData(probeData) {
|
||||
this.index = probeData.index
|
||||
this.ino = probeData.ino || null
|
||||
|
||||
this.path = probeData.path
|
||||
this.fullPath = probeData.fullPath
|
||||
this.ext = probeData.ext
|
||||
@ -92,5 +110,17 @@ class AudioTrack {
|
||||
this.tagTitle = probeData.file_tag_title || null
|
||||
this.tagTrack = probeData.file_tag_track || null
|
||||
}
|
||||
|
||||
syncFile(newFile) {
|
||||
var hasUpdates = false
|
||||
var keysToSync = ['path', 'fullPath', 'ext', 'filename']
|
||||
keysToSync.forEach((key) => {
|
||||
if (newFile[key] !== undefined && newFile[key] !== this[key]) {
|
||||
hasUpdates = true
|
||||
this[key] = newFile[key]
|
||||
}
|
||||
})
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = AudioTrack
|
@ -1,15 +1,20 @@
|
||||
const Path = require('path')
|
||||
const { bytesPretty, elapsedPretty } = require('./utils/fileUtils')
|
||||
const { comparePaths } = require('./utils/index')
|
||||
const { comparePaths, getIno } = require('./utils/index')
|
||||
const Logger = require('./Logger')
|
||||
const Book = require('./Book')
|
||||
const AudioTrack = require('./AudioTrack')
|
||||
const AudioFile = require('./AudioFile')
|
||||
const AudiobookFile = require('./AudiobookFile')
|
||||
|
||||
class Audiobook {
|
||||
constructor(audiobook = null) {
|
||||
this.id = null
|
||||
this.ino = null // Inode
|
||||
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
|
||||
this.addedAt = null
|
||||
this.lastUpdate = null
|
||||
|
||||
@ -30,19 +35,18 @@ class Audiobook {
|
||||
|
||||
construct(audiobook) {
|
||||
this.id = audiobook.id
|
||||
this.ino = audiobook.ino || null
|
||||
|
||||
this.path = audiobook.path
|
||||
this.fullPath = audiobook.fullPath
|
||||
this.addedAt = audiobook.addedAt
|
||||
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
||||
|
||||
this.tracks = audiobook.tracks.map(track => {
|
||||
return new AudioTrack(track)
|
||||
})
|
||||
this.tracks = audiobook.tracks.map(track => new AudioTrack(track))
|
||||
this.missingParts = audiobook.missingParts
|
||||
this.invalidParts = audiobook.invalidParts
|
||||
|
||||
this.audioFiles = audiobook.audioFiles
|
||||
this.otherFiles = audiobook.otherFiles
|
||||
this.audioFiles = audiobook.audioFiles.map(file => new AudioFile(file))
|
||||
this.otherFiles = audiobook.otherFiles.map(file => new AudiobookFile(file))
|
||||
|
||||
this.tags = audiobook.tags
|
||||
if (audiobook.book) {
|
||||
@ -102,6 +106,7 @@ class Audiobook {
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
ino: this.ino,
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
cover: this.cover,
|
||||
@ -114,14 +119,15 @@ class Audiobook {
|
||||
tags: this.tags,
|
||||
book: this.bookToJSON(),
|
||||
tracks: this.tracksToJSON(),
|
||||
audioFiles: this.audioFiles,
|
||||
otherFiles: this.otherFiles
|
||||
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
||||
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON())
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
id: this.id,
|
||||
ino: this.ino,
|
||||
book: this.bookToJSON(),
|
||||
tags: this.tags,
|
||||
path: this.path,
|
||||
@ -140,9 +146,6 @@ class Audiobook {
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
// title: this.title,
|
||||
// author: this.author,
|
||||
// cover: this.cover,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
addedAt: this.addedAt,
|
||||
@ -153,8 +156,8 @@ class Audiobook {
|
||||
sizePretty: this.sizePretty,
|
||||
missingParts: this.missingParts,
|
||||
invalidParts: this.invalidParts,
|
||||
audioFiles: this.audioFiles,
|
||||
otherFiles: this.otherFiles,
|
||||
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
||||
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
|
||||
tags: this.tags,
|
||||
book: this.bookToJSON(),
|
||||
tracks: this.tracksToJSON()
|
||||
@ -175,14 +178,46 @@ class Audiobook {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update was made to add ino values, ensure they are set
|
||||
async checkUpdateInos() {
|
||||
var hasUpdates = false
|
||||
if (!this.ino) {
|
||||
this.ino = await getIno(this.fullPath)
|
||||
hasUpdates = true
|
||||
}
|
||||
for (let i = 0; i < this.audioFiles.length; i++) {
|
||||
var af = this.audioFiles[i]
|
||||
if (!af.ino || af.ino === this.ino) {
|
||||
af.ino = await getIno(af.fullPath)
|
||||
if (!af.ino) {
|
||||
Logger.error('[Audiobook] checkUpdateInos: Failed to set ino for audio file', af.fullPath)
|
||||
} else {
|
||||
var track = this.tracks.find(t => comparePaths(t.path, af.path))
|
||||
if (track) {
|
||||
track.ino = af.ino
|
||||
}
|
||||
}
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
|
||||
this.ino = data.ino || null
|
||||
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.addedAt = Date.now()
|
||||
this.lastUpdate = this.addedAt
|
||||
|
||||
this.otherFiles = data.otherFiles || []
|
||||
if (data.otherFiles) {
|
||||
data.otherFiles.forEach((file) => {
|
||||
this.addOtherFile(file)
|
||||
})
|
||||
}
|
||||
|
||||
this.setBook(data)
|
||||
}
|
||||
|
||||
@ -198,6 +233,20 @@ class Audiobook {
|
||||
return track
|
||||
}
|
||||
|
||||
addAudioFile(audioFileData) {
|
||||
var audioFile = new AudioFile()
|
||||
audioFile.setData(audioFileData)
|
||||
this.audioFiles.push(audioFile)
|
||||
return audioFile
|
||||
}
|
||||
|
||||
addOtherFile(fileData) {
|
||||
var file = new AudiobookFile()
|
||||
file.setData(fileData)
|
||||
this.otherFiles.push(file)
|
||||
return file
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
|
||||
@ -241,17 +290,12 @@ class Audiobook {
|
||||
}
|
||||
|
||||
removeAudioFile(audioFile) {
|
||||
this.tracks = this.tracks.filter(t => t.path !== audioFile.path)
|
||||
this.audioFiles = this.audioFiles.filter(f => f.path !== audioFile.path)
|
||||
}
|
||||
|
||||
audioPartExists(part) {
|
||||
var path = Path.join(this.path, part)
|
||||
return this.audioFiles.find(file => file.path === path)
|
||||
this.tracks = this.tracks.filter(t => t.ino !== audioFile.ino)
|
||||
this.audioFiles = this.audioFiles.filter(f => f.ino !== audioFile.ino)
|
||||
}
|
||||
|
||||
checkUpdateMissingParts() {
|
||||
var currMissingParts = this.missingParts.join(',')
|
||||
var currMissingParts = (this.missingParts || []).join(',') || ''
|
||||
|
||||
var current_index = 1
|
||||
var missingParts = []
|
||||
@ -268,7 +312,8 @@ class Audiobook {
|
||||
|
||||
this.missingParts = missingParts
|
||||
|
||||
var wasUpdated = this.missingParts.join(',') !== currMissingParts
|
||||
var newMissingParts = (this.missingParts || []).join(',') || ''
|
||||
var wasUpdated = newMissingParts !== currMissingParts
|
||||
if (wasUpdated && this.missingParts.length) {
|
||||
Logger.info(`[Audiobook] "${this.title}" has ${missingParts.length} missing parts`)
|
||||
}
|
||||
@ -282,16 +327,18 @@ class Audiobook {
|
||||
|
||||
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
||||
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
||||
|
||||
newOtherFiles.forEach((file) => {
|
||||
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
||||
if (!existingOtherFile) {
|
||||
Logger.info(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
||||
this.otherFiles.push(file)
|
||||
Logger.debug(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
||||
this.addOtherFile(file)
|
||||
}
|
||||
})
|
||||
|
||||
var hasUpdates = currOtherFileNum !== this.otherFiles.length
|
||||
|
||||
// Check if cover was a local image and that it still exists
|
||||
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
|
||||
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
|
||||
var coverStillExists = imageFiles.find(f => comparePaths(f.path, this.book.cover.substr('/local/'.length)))
|
||||
@ -302,6 +349,7 @@ class Audiobook {
|
||||
}
|
||||
}
|
||||
|
||||
// If no cover set and image file exists then use it
|
||||
if (!this.book.cover && imageFiles.length) {
|
||||
this.book.cover = Path.join('/local', imageFiles[0].path)
|
||||
Logger.info(`[Audiobook] Local cover was set | "${this.title}"`)
|
||||
@ -310,8 +358,38 @@ class Audiobook {
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
syncAudioFile(audioFile, fileScanData) {
|
||||
var hasUpdates = audioFile.syncFile(fileScanData)
|
||||
if (hasUpdates) {
|
||||
var track = this.tracks.find(t => t.ino === audioFile.ino)
|
||||
if (track) {
|
||||
track.syncFile(fileScanData)
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
syncPaths(audiobookData) {
|
||||
var hasUpdates = false
|
||||
var keysToSync = ['path', 'fullPath']
|
||||
keysToSync.forEach((key) => {
|
||||
if (audiobookData[key] !== undefined && audiobookData[key] !== this[key]) {
|
||||
hasUpdates = true
|
||||
this[key] = audiobookData[key]
|
||||
}
|
||||
})
|
||||
if (hasUpdates) {
|
||||
this.book.syncPathsUpdated(audiobookData)
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
isSearchMatch(search) {
|
||||
return this.book.isSearchMatch(search.toLowerCase().trim())
|
||||
}
|
||||
|
||||
getAudioFileByIno(ino) {
|
||||
return this.audioFiles.find(af => af.ino === ino)
|
||||
}
|
||||
}
|
||||
module.exports = Audiobook
|
48
server/AudiobookFile.js
Normal file
48
server/AudiobookFile.js
Normal file
@ -0,0 +1,48 @@
|
||||
class AudiobookFile {
|
||||
constructor(data) {
|
||||
this.ino = null
|
||||
this.filetype = null
|
||||
this.filename = null
|
||||
this.ext = null
|
||||
this.path = null
|
||||
this.fullPath = null
|
||||
this.addedAt = null
|
||||
|
||||
if (data) {
|
||||
this.construct(data)
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
ino: this.ino || null,
|
||||
filetype: this.filetype,
|
||||
filename: this.filename,
|
||||
ext: this.ext,
|
||||
path: this.path,
|
||||
fullPath: this.fullPath,
|
||||
addedAt: this.addedAt
|
||||
}
|
||||
}
|
||||
|
||||
construct(data) {
|
||||
this.ino = data.ino || null
|
||||
this.filetype = data.filetype
|
||||
this.filename = data.filename
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.addedAt = data.addedAt
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.ino = data.ino || null
|
||||
this.filetype = data.filetype
|
||||
this.filename = data.filename
|
||||
this.ext = data.ext
|
||||
this.path = data.path
|
||||
this.fullPath = data.fullPath
|
||||
this.addedAt = Date.now()
|
||||
}
|
||||
}
|
||||
module.exports = AudiobookFile
|
@ -129,6 +129,18 @@ class Book {
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
// If audiobook directory path was changed, check and update properties set from dirnames
|
||||
// May be worthwhile checking if these were manually updated and not override manual updates
|
||||
syncPathsUpdated(audiobookData) {
|
||||
var keysToSync = ['author', 'title', 'series', 'publishYear']
|
||||
var syncPayload = {}
|
||||
keysToSync.forEach((key) => {
|
||||
if (audiobookData[key]) syncPayload[key] = audiobookData[key]
|
||||
})
|
||||
if (!Object.keys(syncPayload).length) return false
|
||||
return this.update(syncPayload)
|
||||
}
|
||||
|
||||
isSearchMatch(search) {
|
||||
return this._title.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
|
||||
}
|
||||
|
@ -33,6 +33,11 @@ class Logger {
|
||||
console.info(`[${this.timestamp}] INFO:`, ...args)
|
||||
}
|
||||
|
||||
note(...args) {
|
||||
if (this.LogLevel > LOG_LEVEL.INFO) return
|
||||
console.log(`[${this.timestamp}] NOTE:`, ...args)
|
||||
}
|
||||
|
||||
warn(...args) {
|
||||
if (this.LogLevel > LOG_LEVEL.WARN) return
|
||||
console.warn(`[${this.timestamp}] WARN:`, ...args)
|
||||
|
@ -3,6 +3,7 @@ const BookFinder = require('./BookFinder')
|
||||
const Audiobook = require('./Audiobook')
|
||||
const audioFileScanner = require('./utils/audioFileScanner')
|
||||
const { getAllAudiobookFiles } = require('./utils/scandir')
|
||||
const { comparePaths, getIno } = require('./utils/index')
|
||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||
|
||||
class Scanner {
|
||||
@ -21,12 +22,58 @@ class Scanner {
|
||||
return this.db.audiobooks
|
||||
}
|
||||
|
||||
async setAudiobookDataInos(audiobookData) {
|
||||
for (let i = 0; i < audiobookData.length; i++) {
|
||||
var abd = audiobookData[i]
|
||||
var matchingAB = this.db.audiobooks.find(_ab => comparePaths(_ab.path, abd.path))
|
||||
if (matchingAB) {
|
||||
if (!matchingAB.ino) {
|
||||
matchingAB.ino = await getIno(matchingAB.fullPath)
|
||||
}
|
||||
abd.ino = matchingAB.ino
|
||||
} else {
|
||||
abd.ino = await getIno(abd.fullPath)
|
||||
if (!abd.ino) {
|
||||
Logger.error('[Scanner] Invalid ino - ignoring audiobook data', abd.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return audiobookData.filter(abd => !!abd.ino)
|
||||
}
|
||||
|
||||
async setAudioFileInos(audiobookDataAudioFiles, audiobookAudioFiles) {
|
||||
for (let i = 0; i < audiobookDataAudioFiles.length; i++) {
|
||||
var abdFile = audiobookDataAudioFiles[i]
|
||||
var matchingFile = audiobookAudioFiles.find(af => comparePaths(af.path, abdFile.path))
|
||||
if (matchingFile) {
|
||||
if (!matchingFile.ino) {
|
||||
matchingFile.ino = await getIno(matchingFile.fullPath)
|
||||
}
|
||||
abdFile.ino = matchingFile.ino
|
||||
} else {
|
||||
abdFile.ino = await getIno(abdFile.fullPath)
|
||||
if (!abdFile.ino) {
|
||||
Logger.error('[Scanner] Invalid abdFile ino - ignoring abd audio file', abdFile.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino)
|
||||
}
|
||||
|
||||
async scan() {
|
||||
// TEMP - fix relative file paths
|
||||
// TEMP - update ino for each audiobook
|
||||
if (this.audiobooks.length) {
|
||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||
var ab = this.audiobooks[i]
|
||||
if (ab.fixRelativePath(this.AudiobookPath)) {
|
||||
var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
|
||||
|
||||
// Update ino if an audio file has the same ino as the audiobook
|
||||
var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino)
|
||||
if (shouldUpdateIno) {
|
||||
await ab.checkUpdateInos()
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
await this.db.updateAudiobook(ab)
|
||||
}
|
||||
}
|
||||
@ -35,6 +82,9 @@ class Scanner {
|
||||
const scanStart = Date.now()
|
||||
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
|
||||
|
||||
// Set ino for each ab data as a string
|
||||
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)
|
||||
|
||||
if (this.cancelScan) {
|
||||
this.cancelScan = false
|
||||
return null
|
||||
@ -48,17 +98,13 @@ class Scanner {
|
||||
|
||||
// Check for removed audiobooks
|
||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||
var dataFound = audiobookDataFound.find(abd => abd.path === this.audiobooks[i].path)
|
||||
var dataFound = audiobookDataFound.find(abd => abd.ino === this.audiobooks[i].ino)
|
||||
if (!dataFound) {
|
||||
Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`)
|
||||
|
||||
var audiobookJSON = this.audiobooks[i].toJSONMinified()
|
||||
await this.db.removeEntity('audiobook', this.audiobooks[i].id)
|
||||
if (!this.audiobooks[i]) {
|
||||
Logger.error('[Scanner] Oops... audiobook is now invalid...')
|
||||
continue;
|
||||
}
|
||||
scanResults.removed++
|
||||
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
|
||||
this.emitter('audiobook_removed', audiobookJSON)
|
||||
}
|
||||
if (this.cancelScan) {
|
||||
this.cancelScan = false
|
||||
@ -68,38 +114,44 @@ class Scanner {
|
||||
|
||||
for (let i = 0; i < audiobookDataFound.length; i++) {
|
||||
var audiobookData = audiobookDataFound[i]
|
||||
var existingAudiobook = this.audiobooks.find(a => a.fullPath === audiobookData.fullPath)
|
||||
if (existingAudiobook) {
|
||||
Logger.debug(`[Scanner] Audiobook already added, check updates for "${existingAudiobook.title}"`)
|
||||
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
||||
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
||||
|
||||
if (!audiobookData.parts.length) {
|
||||
if (existingAudiobook) {
|
||||
if (!audiobookData.audioFiles.length) {
|
||||
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
|
||||
|
||||
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
||||
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
||||
scanResults.removed++
|
||||
} else {
|
||||
audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles)
|
||||
var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino)
|
||||
|
||||
// Check for audio files that were removed
|
||||
var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !audiobookData.parts.includes(file.filename))
|
||||
var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !abdAudioFileInos.includes(file.ino))
|
||||
if (removedAudioFiles.length) {
|
||||
Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`)
|
||||
removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af))
|
||||
}
|
||||
|
||||
// Check for audio files that were added
|
||||
var newParts = audiobookData.parts.filter(part => !existingAudiobook.audioPartExists(part))
|
||||
if (newParts.length) {
|
||||
Logger.info(`[Scanner] ${newParts.length} new audio parts were found for audiobook "${existingAudiobook.title}"`)
|
||||
|
||||
// If previously invalid part, remove from invalid list because it will be re-scanned
|
||||
newParts.forEach((part) => {
|
||||
if (existingAudiobook.invalidParts.includes(part)) {
|
||||
existingAudiobook.invalidParts = existingAudiobook.invalidParts.filter(p => p !== part)
|
||||
// Check for new audio files and sync existing audio files
|
||||
var newAudioFiles = []
|
||||
var hasUpdatedAudioFiles = false
|
||||
audiobookData.audioFiles.forEach((file) => {
|
||||
var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino)
|
||||
if (existingAudioFile) { // Audio file exists, sync paths
|
||||
if (existingAudiobook.syncAudioFile(existingAudioFile, file)) {
|
||||
hasUpdatedAudioFiles = true
|
||||
}
|
||||
})
|
||||
// Scan new audio parts found
|
||||
await audioFileScanner.scanParts(existingAudiobook, newParts)
|
||||
} else {
|
||||
newAudioFiles.push(file)
|
||||
}
|
||||
})
|
||||
if (newAudioFiles.length) {
|
||||
Logger.info(`[Scanner] ${newAudioFiles.length} new audio files were found for audiobook "${existingAudiobook.title}"`)
|
||||
// Scan new audio files found
|
||||
await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles)
|
||||
}
|
||||
|
||||
if (!existingAudiobook.tracks.length) {
|
||||
@ -108,7 +160,7 @@ class Scanner {
|
||||
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
||||
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
||||
} else {
|
||||
var hasUpdates = removedAudioFiles.length || newParts.length
|
||||
var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles
|
||||
|
||||
if (existingAudiobook.checkUpdateMissingParts()) {
|
||||
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
|
||||
@ -119,6 +171,11 @@ class Scanner {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
// Syncs path and fullPath
|
||||
if (existingAudiobook.syncPaths(audiobookData)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
||||
existingAudiobook.lastUpdate = Date.now()
|
||||
@ -129,12 +186,12 @@ class Scanner {
|
||||
}
|
||||
} // end if update existing
|
||||
} else {
|
||||
if (!audiobookData.parts.length) {
|
||||
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData)
|
||||
if (!audiobookData.audioFiles.length) {
|
||||
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
|
||||
} else {
|
||||
var audiobook = new Audiobook()
|
||||
audiobook.setData(audiobookData)
|
||||
await audioFileScanner.scanParts(audiobook, audiobookData.parts)
|
||||
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
||||
if (!audiobook.tracks.length) {
|
||||
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
||||
} else {
|
||||
|
@ -18,9 +18,9 @@ class Server {
|
||||
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||
this.Port = PORT
|
||||
this.Host = '0.0.0.0'
|
||||
this.ConfigPath = CONFIG_PATH
|
||||
this.AudiobookPath = AUDIOBOOK_PATH
|
||||
this.MetadataPath = METADATA_PATH
|
||||
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||
this.MetadataPath = Path.normalize(METADATA_PATH)
|
||||
|
||||
fs.ensureDirSync(CONFIG_PATH)
|
||||
fs.ensureDirSync(METADATA_PATH)
|
||||
|
@ -1,6 +1,6 @@
|
||||
var EventEmitter = require('events')
|
||||
var Logger = require('./Logger')
|
||||
var chokidar = require('chokidar')
|
||||
var Watcher = require('watcher')
|
||||
|
||||
class FolderWatcher extends EventEmitter {
|
||||
constructor(audiobookPath) {
|
||||
@ -12,15 +12,14 @@ class FolderWatcher extends EventEmitter {
|
||||
|
||||
initWatcher() {
|
||||
try {
|
||||
Logger.info('[WATCHER] Initializing..')
|
||||
this.watcher = chokidar.watch(this.AudiobookPath, {
|
||||
ignoreInitial: true,
|
||||
Logger.info('[FolderWatcher] Initializing..')
|
||||
this.watcher = new Watcher(this.AudiobookPath, {
|
||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 2500,
|
||||
pollInterval: 500
|
||||
}
|
||||
renameDetection: true,
|
||||
renameTimeout: 2000,
|
||||
recursive: true,
|
||||
ignoreInitial: true,
|
||||
persistent: true
|
||||
})
|
||||
this.watcher
|
||||
.on('add', (path) => {
|
||||
@ -29,10 +28,12 @@ class FolderWatcher extends EventEmitter {
|
||||
this.onFileUpdated(path)
|
||||
}).on('unlink', path => {
|
||||
this.onFileRemoved(path)
|
||||
}).on('rename', (path, pathNext) => {
|
||||
this.onRename(path, pathNext)
|
||||
}).on('error', (error) => {
|
||||
Logger.error(`Watcher error: ${error}`)
|
||||
Logger.error(`[FolderWatcher] ${error}`)
|
||||
}).on('ready', () => {
|
||||
Logger.info('[WATCHER] Ready')
|
||||
Logger.info('[FolderWatcher] Ready')
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error('Chokidar watcher failed', error)
|
||||
@ -53,7 +54,7 @@ class FolderWatcher extends EventEmitter {
|
||||
}
|
||||
|
||||
onFileRemoved(path) {
|
||||
Logger.debug('FolderWatcher: File Removed', path)
|
||||
Logger.debug('[FolderWatcher] File Removed', path)
|
||||
this.emit('file_removed', {
|
||||
path: path.replace(this.AudiobookPath, ''),
|
||||
fullPath: path
|
||||
@ -61,11 +62,15 @@ class FolderWatcher extends EventEmitter {
|
||||
}
|
||||
|
||||
onFileUpdated(path) {
|
||||
Logger.debug('FolderWatcher: Updated File', path)
|
||||
Logger.debug('[FolderWatcher] Updated File', path)
|
||||
this.emit('file_updated', {
|
||||
path: path.replace(this.AudiobookPath, ''),
|
||||
fullPath: path
|
||||
})
|
||||
}
|
||||
|
||||
onRename(pathFrom, pathTo) {
|
||||
Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`)
|
||||
}
|
||||
}
|
||||
module.exports = FolderWatcher
|
@ -26,6 +26,12 @@ class OpenLibrary {
|
||||
|
||||
async getWorksData(worksKey) {
|
||||
var worksData = await this.get(`${worksKey}.json`)
|
||||
if (!worksData) {
|
||||
return {
|
||||
errorMsg: 'Works Data Request failed',
|
||||
errorCode: 500
|
||||
}
|
||||
}
|
||||
if (!worksData.covers) worksData.covers = []
|
||||
var coverImages = worksData.covers.filter(c => c > 0).map(c => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
||||
var description = null
|
||||
|
@ -1,6 +1,7 @@
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
var prober = require('./prober')
|
||||
const prober = require('./prober')
|
||||
const AudioFile = require('../AudioFile')
|
||||
|
||||
|
||||
function getDefaultAudioStream(audioStreams) {
|
||||
@ -76,41 +77,42 @@ function getTrackNumberFromFilename(filename) {
|
||||
return number
|
||||
}
|
||||
|
||||
async function scanParts(audiobook, parts) {
|
||||
if (!parts || !parts.length) {
|
||||
Logger.error('[AudioFileScanner] Scan Parts', audiobook.title, 'No Parts', parts)
|
||||
async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||
if (!newAudioFiles || !newAudioFiles.length) {
|
||||
Logger.error('[AudioFileScanner] Scan Audio Files no files', audiobook.title)
|
||||
return
|
||||
}
|
||||
var tracks = []
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
var fullPath = Path.join(audiobook.fullPath, parts[i])
|
||||
for (let i = 0; i < newAudioFiles.length; i++) {
|
||||
var audioFile = newAudioFiles[i]
|
||||
|
||||
var scanData = await scan(fullPath)
|
||||
var scanData = await scan(audioFile.fullPath)
|
||||
if (!scanData || scanData.error) {
|
||||
Logger.error('[AudioFileScanner] Scan failed for', parts[i])
|
||||
audiobook.invalidParts.push(parts[i])
|
||||
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
||||
// audiobook.invalidAudioFiles.push(parts[i])
|
||||
continue;
|
||||
}
|
||||
|
||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(parts[i])
|
||||
var trackNumFromFilename = getTrackNumberFromFilename(audioFile.filename)
|
||||
|
||||
var audioFileObj = {
|
||||
path: Path.join(audiobook.path, parts[i]),
|
||||
ext: Path.extname(parts[i]),
|
||||
filename: parts[i],
|
||||
fullPath: fullPath,
|
||||
ino: audioFile.ino,
|
||||
filename: audioFile.filename,
|
||||
path: audioFile.path,
|
||||
fullPath: audioFile.fullPath,
|
||||
ext: audioFile.ext,
|
||||
...scanData,
|
||||
trackNumFromMeta,
|
||||
trackNumFromFilename
|
||||
}
|
||||
audiobook.audioFiles.push(audioFileObj)
|
||||
audiobook.addAudioFile(audioFileObj)
|
||||
|
||||
var trackNumber = 1
|
||||
if (parts.length > 1) {
|
||||
if (newAudioFiles.length > 1) {
|
||||
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
||||
if (trackNumber === null) {
|
||||
Logger.error('[AudioFileScanner] Invalid track number for', parts[i])
|
||||
Logger.error('[AudioFileScanner] Invalid track number for', audioFile.filename)
|
||||
audioFileObj.invalid = true
|
||||
audioFileObj.error = 'Failed to get track number'
|
||||
continue;
|
||||
@ -118,7 +120,7 @@ async function scanParts(audiobook, parts) {
|
||||
}
|
||||
|
||||
if (tracks.find(t => t.index === trackNumber)) {
|
||||
Logger.error('[AudioFileScanner] Duplicate track number for', parts[i])
|
||||
Logger.error('[AudioFileScanner] Duplicate track number for', audioFile.filename)
|
||||
audioFileObj.invalid = true
|
||||
audioFileObj.error = 'Duplicate track number'
|
||||
continue;
|
||||
@ -156,4 +158,4 @@ async function scanParts(audiobook, parts) {
|
||||
audiobook.tracks.sort((a, b) => a.index - b.index)
|
||||
}
|
||||
}
|
||||
module.exports.scanParts = scanParts
|
||||
module.exports.scanAudioFiles = scanAudioFiles
|
@ -1,4 +1,6 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
||||
if (!caseSensitive) {
|
||||
@ -48,24 +50,13 @@ module.exports.isObject = (val) => {
|
||||
return val !== null && typeof val === 'object'
|
||||
}
|
||||
|
||||
function normalizePath(path) {
|
||||
const replace = [
|
||||
[/\\/g, '/'],
|
||||
[/(\w):/, '/$1'],
|
||||
[/(\w+)\/\.\.\/?/g, ''],
|
||||
[/^\.\//, ''],
|
||||
[/\/\.\//, '/'],
|
||||
[/\/\.$/, ''],
|
||||
[/\/$/, ''],
|
||||
]
|
||||
replace.forEach(array => {
|
||||
while (array[0].test(path)) {
|
||||
path = path.replace(array[0], array[1])
|
||||
}
|
||||
})
|
||||
return path
|
||||
module.exports.comparePaths = (path1, path2) => {
|
||||
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
|
||||
}
|
||||
|
||||
module.exports.comparePaths = (path1, path2) => {
|
||||
return (path1 === path2) || (normalizePath(path1) === normalizePath(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, error)
|
||||
return null
|
||||
})
|
||||
}
|
@ -3,7 +3,7 @@ const dir = require('node-dir')
|
||||
const Logger = require('../Logger')
|
||||
const { cleanString } = require('./index')
|
||||
|
||||
const AUDIOBOOK_PARTS_FORMATS = ['m4b', 'mp3']
|
||||
const AUDIO_FORMATS = ['m4b', 'mp3']
|
||||
const INFO_FORMATS = ['nfo']
|
||||
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp']
|
||||
const EBOOK_FORMATS = ['epub', 'pdf']
|
||||
@ -23,7 +23,7 @@ function getPaths(path) {
|
||||
function getFileType(ext) {
|
||||
var ext_cleaned = ext.toLowerCase()
|
||||
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
||||
if (AUDIOBOOK_PARTS_FORMATS.includes(ext_cleaned)) return 'abpart'
|
||||
if (AUDIO_FORMATS.includes(ext_cleaned)) return 'audio'
|
||||
if (INFO_FORMATS.includes(ext_cleaned)) return 'info'
|
||||
if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image'
|
||||
if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook'
|
||||
@ -35,7 +35,7 @@ async function getAllAudiobookFiles(abRootPath) {
|
||||
var audiobooks = {}
|
||||
|
||||
paths.files.forEach((filepath) => {
|
||||
var relpath = filepath.replace(abRootPath, '').slice(1)
|
||||
var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1)
|
||||
var pathformat = Path.parse(relpath)
|
||||
var path = pathformat.dir
|
||||
|
||||
@ -71,22 +71,20 @@ async function getAllAudiobookFiles(abRootPath) {
|
||||
publishYear: publishYear,
|
||||
path: path,
|
||||
fullPath: Path.join(abRootPath, path),
|
||||
parts: [],
|
||||
audioFiles: [],
|
||||
otherFiles: []
|
||||
}
|
||||
}
|
||||
|
||||
var filetype = getFileType(pathformat.ext)
|
||||
if (filetype === 'abpart') {
|
||||
audiobooks[path].parts.push(pathformat.base)
|
||||
var fileObj = {
|
||||
filetype: getFileType(pathformat.ext),
|
||||
filename: pathformat.base,
|
||||
path: relpath,
|
||||
fullPath: filepath,
|
||||
ext: pathformat.ext
|
||||
}
|
||||
if (fileObj.filetype === 'audio') {
|
||||
audiobooks[path].audioFiles.push(fileObj)
|
||||
} else {
|
||||
var fileObj = {
|
||||
filetype: filetype,
|
||||
filename: pathformat.base,
|
||||
path: relpath,
|
||||
fullPath: filepath,
|
||||
ext: pathformat.ext
|
||||
}
|
||||
audiobooks[path].otherFiles.push(fileObj)
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user