mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +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",
|
"name": "audiobookshelf-client",
|
||||||
"version": "0.9.75-beta",
|
"version": "0.9.77-beta",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -4,7 +4,8 @@ module.exports = {
|
|||||||
purge: {
|
purge: {
|
||||||
options: {
|
options: {
|
||||||
safelist: [
|
safelist: [
|
||||||
'bg-success'
|
'bg-success',
|
||||||
|
'bg-red-600'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
166
package-lock.json
generated
166
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "0.9.72-beta",
|
"version": "0.9.76-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -69,6 +69,11 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"accepts": {
|
||||||
"version": "1.3.7",
|
"version": "1.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||||
@ -78,13 +83,12 @@
|
|||||||
"negotiator": "0.6.2"
|
"negotiator": "0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anymatch": {
|
"are-shallow-equal": {
|
||||||
"version": "3.1.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/are-shallow-equal/-/are-shallow-equal-1.1.1.tgz",
|
||||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
"integrity": "sha512-Y0MC/7IP+WZSo0NgYDwww7euKssEodUJxjby3fmNurEDcbq8htqSgyI7a7HELJzkzNv26dOH5vKQFlzCt1H9Ag==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"normalize-path": "^3.0.0",
|
"is-primitive": "^3.0.1"
|
||||||
"picomatch": "^2.0.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
@ -97,6 +101,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
|
||||||
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
|
"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": {
|
"axios": {
|
||||||
"version": "0.21.1",
|
"version": "0.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
|
"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": {
|
"body-parser": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||||
@ -156,14 +160,6 @@
|
|||||||
"concat-map": "0.0.1"
|
"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": {
|
"buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"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"
|
"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": {
|
"clone-response": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||||
@ -267,6 +248,11 @@
|
|||||||
"vary": "^1"
|
"vary": "^1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"debounce": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@ -420,14 +406,6 @@
|
|||||||
"vary": "~1.1.2"
|
"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": {
|
"finalhandler": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||||
@ -476,12 +454,6 @@
|
|||||||
"universalify": "^2.0.0"
|
"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": {
|
"get-stream": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
@ -490,14 +462,6 @@
|
|||||||
"pump": "^3.0.0"
|
"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": {
|
"got": {
|
||||||
"version": "11.3.0",
|
"version": "11.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/got/-/got-11.3.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||||
},
|
},
|
||||||
"is-binary-path": {
|
"is-primitive": {
|
||||||
"version": "2.1.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="
|
||||||
"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=="
|
|
||||||
},
|
},
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -787,11 +730,6 @@
|
|||||||
"minimatch": "^3.0.2"
|
"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": {
|
"normalize-url": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
"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": {
|
"podcast": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/podcast/-/podcast-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/podcast/-/podcast-1.3.0.tgz",
|
||||||
@ -846,6 +779,11 @@
|
|||||||
"rss": "^1.2.2"
|
"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": {
|
"proper-lockfile": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||||
@ -900,14 +838,6 @@
|
|||||||
"unpipe": "1.0.0"
|
"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": {
|
"resolve-alpn": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
"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": {
|
"rss": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||||
},
|
},
|
||||||
"to-regex-range": {
|
"string-indexes": {
|
||||||
"version": "5.0.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string-indexes/-/string-indexes-1.0.0.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"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": {
|
"requires": {
|
||||||
"is-number": "^7.0.0"
|
"promise-concurrency-limiter": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toidentifier": {
|
"toidentifier": {
|
||||||
@ -1121,6 +1064,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
"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": {
|
"which": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "0.9.75-beta",
|
"version": "0.9.77-beta",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -12,7 +12,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chokidar": "^3.5.2",
|
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
@ -23,7 +22,8 @@
|
|||||||
"njodb": "^0.4.20",
|
"njodb": "^0.4.20",
|
||||||
"node-dir": "^0.1.17",
|
"node-dir": "^0.1.17",
|
||||||
"podcast": "^1.3.0",
|
"podcast": "^1.3.0",
|
||||||
"socket.io": "^4.1.3"
|
"socket.io": "^4.1.3",
|
||||||
|
"watcher": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"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 {
|
class AudioTrack {
|
||||||
constructor(audioTrack = null) {
|
constructor(audioTrack = null) {
|
||||||
this.index = null
|
this.index = null
|
||||||
|
this.ino = null
|
||||||
|
|
||||||
this.path = null
|
this.path = null
|
||||||
this.fullPath = null
|
this.fullPath = null
|
||||||
this.ext = null
|
this.ext = null
|
||||||
@ -31,6 +33,8 @@ class AudioTrack {
|
|||||||
|
|
||||||
construct(audioTrack) {
|
construct(audioTrack) {
|
||||||
this.index = audioTrack.index
|
this.index = audioTrack.index
|
||||||
|
this.ino = audioTrack.ino || null
|
||||||
|
|
||||||
this.path = audioTrack.path
|
this.path = audioTrack.path
|
||||||
this.fullPath = audioTrack.fullPath
|
this.fullPath = audioTrack.fullPath
|
||||||
this.ext = audioTrack.ext
|
this.ext = audioTrack.ext
|
||||||
@ -45,6 +49,12 @@ class AudioTrack {
|
|||||||
this.timeBase = audioTrack.timeBase
|
this.timeBase = audioTrack.timeBase
|
||||||
this.channels = audioTrack.channels
|
this.channels = audioTrack.channels
|
||||||
this.channelLayout = audioTrack.channelLayout
|
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() {
|
get name() {
|
||||||
@ -54,6 +64,7 @@ class AudioTrack {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
index: this.index,
|
index: this.index,
|
||||||
|
ino: this.ino,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
fullPath: this.fullPath,
|
fullPath: this.fullPath,
|
||||||
ext: this.ext,
|
ext: this.ext,
|
||||||
@ -65,12 +76,19 @@ class AudioTrack {
|
|||||||
language: this.language,
|
language: this.language,
|
||||||
timeBase: this.timeBase,
|
timeBase: this.timeBase,
|
||||||
channels: this.channels,
|
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) {
|
setData(probeData) {
|
||||||
this.index = probeData.index
|
this.index = probeData.index
|
||||||
|
this.ino = probeData.ino || null
|
||||||
|
|
||||||
this.path = probeData.path
|
this.path = probeData.path
|
||||||
this.fullPath = probeData.fullPath
|
this.fullPath = probeData.fullPath
|
||||||
this.ext = probeData.ext
|
this.ext = probeData.ext
|
||||||
@ -92,5 +110,17 @@ class AudioTrack {
|
|||||||
this.tagTitle = probeData.file_tag_title || null
|
this.tagTitle = probeData.file_tag_title || null
|
||||||
this.tagTrack = probeData.file_tag_track || 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
|
module.exports = AudioTrack
|
@ -1,15 +1,20 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { bytesPretty, elapsedPretty } = require('./utils/fileUtils')
|
const { bytesPretty, elapsedPretty } = require('./utils/fileUtils')
|
||||||
const { comparePaths } = require('./utils/index')
|
const { comparePaths, getIno } = require('./utils/index')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const Book = require('./Book')
|
const Book = require('./Book')
|
||||||
const AudioTrack = require('./AudioTrack')
|
const AudioTrack = require('./AudioTrack')
|
||||||
|
const AudioFile = require('./AudioFile')
|
||||||
|
const AudiobookFile = require('./AudiobookFile')
|
||||||
|
|
||||||
class Audiobook {
|
class Audiobook {
|
||||||
constructor(audiobook = null) {
|
constructor(audiobook = null) {
|
||||||
this.id = null
|
this.id = null
|
||||||
|
this.ino = null // Inode
|
||||||
|
|
||||||
this.path = null
|
this.path = null
|
||||||
this.fullPath = null
|
this.fullPath = null
|
||||||
|
|
||||||
this.addedAt = null
|
this.addedAt = null
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
|
|
||||||
@ -30,19 +35,18 @@ class Audiobook {
|
|||||||
|
|
||||||
construct(audiobook) {
|
construct(audiobook) {
|
||||||
this.id = audiobook.id
|
this.id = audiobook.id
|
||||||
|
this.ino = audiobook.ino || null
|
||||||
|
|
||||||
this.path = audiobook.path
|
this.path = audiobook.path
|
||||||
this.fullPath = audiobook.fullPath
|
this.fullPath = audiobook.fullPath
|
||||||
this.addedAt = audiobook.addedAt
|
this.addedAt = audiobook.addedAt
|
||||||
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
this.lastUpdate = audiobook.lastUpdate || this.addedAt
|
||||||
|
|
||||||
this.tracks = audiobook.tracks.map(track => {
|
this.tracks = audiobook.tracks.map(track => new AudioTrack(track))
|
||||||
return new AudioTrack(track)
|
|
||||||
})
|
|
||||||
this.missingParts = audiobook.missingParts
|
this.missingParts = audiobook.missingParts
|
||||||
this.invalidParts = audiobook.invalidParts
|
|
||||||
|
|
||||||
this.audioFiles = audiobook.audioFiles
|
this.audioFiles = audiobook.audioFiles.map(file => new AudioFile(file))
|
||||||
this.otherFiles = audiobook.otherFiles
|
this.otherFiles = audiobook.otherFiles.map(file => new AudiobookFile(file))
|
||||||
|
|
||||||
this.tags = audiobook.tags
|
this.tags = audiobook.tags
|
||||||
if (audiobook.book) {
|
if (audiobook.book) {
|
||||||
@ -102,6 +106,7 @@ class Audiobook {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
author: this.author,
|
author: this.author,
|
||||||
cover: this.cover,
|
cover: this.cover,
|
||||||
@ -114,14 +119,15 @@ class Audiobook {
|
|||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
tracks: this.tracksToJSON(),
|
tracks: this.tracksToJSON(),
|
||||||
audioFiles: this.audioFiles,
|
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
||||||
otherFiles: this.otherFiles
|
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSONMinified() {
|
toJSONMinified() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
@ -140,9 +146,6 @@ class Audiobook {
|
|||||||
toJSONExpanded() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
// title: this.title,
|
|
||||||
// author: this.author,
|
|
||||||
// cover: this.cover,
|
|
||||||
path: this.path,
|
path: this.path,
|
||||||
fullPath: this.fullPath,
|
fullPath: this.fullPath,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
@ -153,8 +156,8 @@ class Audiobook {
|
|||||||
sizePretty: this.sizePretty,
|
sizePretty: this.sizePretty,
|
||||||
missingParts: this.missingParts,
|
missingParts: this.missingParts,
|
||||||
invalidParts: this.invalidParts,
|
invalidParts: this.invalidParts,
|
||||||
audioFiles: this.audioFiles,
|
audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()),
|
||||||
otherFiles: this.otherFiles,
|
otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()),
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
tracks: this.tracksToJSON()
|
tracks: this.tracksToJSON()
|
||||||
@ -175,14 +178,46 @@ class Audiobook {
|
|||||||
return false
|
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) {
|
setData(data) {
|
||||||
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
|
this.id = (Math.trunc(Math.random() * 1000) + Date.now()).toString(36)
|
||||||
|
this.ino = data.ino || null
|
||||||
|
|
||||||
this.path = data.path
|
this.path = data.path
|
||||||
this.fullPath = data.fullPath
|
this.fullPath = data.fullPath
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.lastUpdate = this.addedAt
|
this.lastUpdate = this.addedAt
|
||||||
|
|
||||||
this.otherFiles = data.otherFiles || []
|
if (data.otherFiles) {
|
||||||
|
data.otherFiles.forEach((file) => {
|
||||||
|
this.addOtherFile(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.setBook(data)
|
this.setBook(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +233,20 @@ class Audiobook {
|
|||||||
return track
|
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) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
@ -241,17 +290,12 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeAudioFile(audioFile) {
|
removeAudioFile(audioFile) {
|
||||||
this.tracks = this.tracks.filter(t => t.path !== audioFile.path)
|
this.tracks = this.tracks.filter(t => t.ino !== audioFile.ino)
|
||||||
this.audioFiles = this.audioFiles.filter(f => f.path !== audioFile.path)
|
this.audioFiles = this.audioFiles.filter(f => f.ino !== audioFile.ino)
|
||||||
}
|
|
||||||
|
|
||||||
audioPartExists(part) {
|
|
||||||
var path = Path.join(this.path, part)
|
|
||||||
return this.audioFiles.find(file => file.path === path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkUpdateMissingParts() {
|
checkUpdateMissingParts() {
|
||||||
var currMissingParts = this.missingParts.join(',')
|
var currMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
|
|
||||||
var current_index = 1
|
var current_index = 1
|
||||||
var missingParts = []
|
var missingParts = []
|
||||||
@ -268,7 +312,8 @@ class Audiobook {
|
|||||||
|
|
||||||
this.missingParts = missingParts
|
this.missingParts = missingParts
|
||||||
|
|
||||||
var wasUpdated = this.missingParts.join(',') !== currMissingParts
|
var newMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
|
var wasUpdated = newMissingParts !== currMissingParts
|
||||||
if (wasUpdated && this.missingParts.length) {
|
if (wasUpdated && this.missingParts.length) {
|
||||||
Logger.info(`[Audiobook] "${this.title}" has ${missingParts.length} missing parts`)
|
Logger.info(`[Audiobook] "${this.title}" has ${missingParts.length} missing parts`)
|
||||||
}
|
}
|
||||||
@ -282,16 +327,18 @@ class Audiobook {
|
|||||||
|
|
||||||
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
var newOtherFilePaths = newOtherFiles.map(f => f.path)
|
||||||
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
|
||||||
|
|
||||||
newOtherFiles.forEach((file) => {
|
newOtherFiles.forEach((file) => {
|
||||||
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
var existingOtherFile = this.otherFiles.find(f => f.path === file.path)
|
||||||
if (!existingOtherFile) {
|
if (!existingOtherFile) {
|
||||||
Logger.info(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
Logger.debug(`[Audiobook] New other file found on sync ${file.filename}/${file.filetype} | "${this.title}"`)
|
||||||
this.otherFiles.push(file)
|
this.addOtherFile(file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var hasUpdates = currOtherFileNum !== this.otherFiles.length
|
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')
|
var imageFiles = this.otherFiles.filter(f => f.filetype === 'image')
|
||||||
if (this.book.cover && this.book.cover.substr(1).startsWith('local')) {
|
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)))
|
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) {
|
if (!this.book.cover && imageFiles.length) {
|
||||||
this.book.cover = Path.join('/local', imageFiles[0].path)
|
this.book.cover = Path.join('/local', imageFiles[0].path)
|
||||||
Logger.info(`[Audiobook] Local cover was set | "${this.title}"`)
|
Logger.info(`[Audiobook] Local cover was set | "${this.title}"`)
|
||||||
@ -310,8 +358,38 @@ class Audiobook {
|
|||||||
return hasUpdates
|
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) {
|
isSearchMatch(search) {
|
||||||
return this.book.isSearchMatch(search.toLowerCase().trim())
|
return this.book.isSearchMatch(search.toLowerCase().trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioFileByIno(ino) {
|
||||||
|
return this.audioFiles.find(af => af.ino === ino)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Audiobook
|
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
|
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) {
|
isSearchMatch(search) {
|
||||||
return this._title.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(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)
|
console.info(`[${this.timestamp}] INFO:`, ...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
note(...args) {
|
||||||
|
if (this.LogLevel > LOG_LEVEL.INFO) return
|
||||||
|
console.log(`[${this.timestamp}] NOTE:`, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
warn(...args) {
|
warn(...args) {
|
||||||
if (this.LogLevel > LOG_LEVEL.WARN) return
|
if (this.LogLevel > LOG_LEVEL.WARN) return
|
||||||
console.warn(`[${this.timestamp}] WARN:`, ...args)
|
console.warn(`[${this.timestamp}] WARN:`, ...args)
|
||||||
|
@ -3,6 +3,7 @@ const BookFinder = require('./BookFinder')
|
|||||||
const Audiobook = require('./Audiobook')
|
const Audiobook = require('./Audiobook')
|
||||||
const audioFileScanner = require('./utils/audioFileScanner')
|
const audioFileScanner = require('./utils/audioFileScanner')
|
||||||
const { getAllAudiobookFiles } = require('./utils/scandir')
|
const { getAllAudiobookFiles } = require('./utils/scandir')
|
||||||
|
const { comparePaths, getIno } = require('./utils/index')
|
||||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
@ -21,12 +22,58 @@ class Scanner {
|
|||||||
return this.db.audiobooks
|
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() {
|
async scan() {
|
||||||
// TEMP - fix relative file paths
|
// TEMP - fix relative file paths
|
||||||
|
// TEMP - update ino for each audiobook
|
||||||
if (this.audiobooks.length) {
|
if (this.audiobooks.length) {
|
||||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||||
var ab = this.audiobooks[i]
|
var ab = this.audiobooks[i]
|
||||||
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)
|
await this.db.updateAudiobook(ab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,6 +82,9 @@ class Scanner {
|
|||||||
const scanStart = Date.now()
|
const scanStart = Date.now()
|
||||||
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
|
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
|
||||||
|
|
||||||
|
// Set ino for each ab data as a string
|
||||||
|
audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound)
|
||||||
|
|
||||||
if (this.cancelScan) {
|
if (this.cancelScan) {
|
||||||
this.cancelScan = false
|
this.cancelScan = false
|
||||||
return null
|
return null
|
||||||
@ -48,17 +98,13 @@ class Scanner {
|
|||||||
|
|
||||||
// Check for removed audiobooks
|
// Check for removed audiobooks
|
||||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||||
var dataFound = audiobookDataFound.find(abd => abd.path === this.audiobooks[i].path)
|
var dataFound = audiobookDataFound.find(abd => abd.ino === this.audiobooks[i].ino)
|
||||||
if (!dataFound) {
|
if (!dataFound) {
|
||||||
Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`)
|
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)
|
await this.db.removeEntity('audiobook', this.audiobooks[i].id)
|
||||||
if (!this.audiobooks[i]) {
|
|
||||||
Logger.error('[Scanner] Oops... audiobook is now invalid...')
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
scanResults.removed++
|
scanResults.removed++
|
||||||
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
|
this.emitter('audiobook_removed', audiobookJSON)
|
||||||
}
|
}
|
||||||
if (this.cancelScan) {
|
if (this.cancelScan) {
|
||||||
this.cancelScan = false
|
this.cancelScan = false
|
||||||
@ -68,38 +114,44 @@ class Scanner {
|
|||||||
|
|
||||||
for (let i = 0; i < audiobookDataFound.length; i++) {
|
for (let i = 0; i < audiobookDataFound.length; i++) {
|
||||||
var audiobookData = audiobookDataFound[i]
|
var audiobookData = audiobookDataFound[i]
|
||||||
var existingAudiobook = this.audiobooks.find(a => a.fullPath === audiobookData.fullPath)
|
var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino)
|
||||||
if (existingAudiobook) {
|
Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
|
||||||
Logger.debug(`[Scanner] Audiobook already added, check updates for "${existingAudiobook.title}"`)
|
|
||||||
|
|
||||||
if (!audiobookData.parts.length) {
|
if (existingAudiobook) {
|
||||||
|
if (!audiobookData.audioFiles.length) {
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
|
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
|
||||||
|
|
||||||
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
||||||
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
||||||
scanResults.removed++
|
scanResults.removed++
|
||||||
} else {
|
} 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
|
// 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) {
|
if (removedAudioFiles.length) {
|
||||||
Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`)
|
Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`)
|
||||||
removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af))
|
removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for audio files that were added
|
// Check for new audio files and sync existing audio files
|
||||||
var newParts = audiobookData.parts.filter(part => !existingAudiobook.audioPartExists(part))
|
var newAudioFiles = []
|
||||||
if (newParts.length) {
|
var hasUpdatedAudioFiles = false
|
||||||
Logger.info(`[Scanner] ${newParts.length} new audio parts were found for audiobook "${existingAudiobook.title}"`)
|
audiobookData.audioFiles.forEach((file) => {
|
||||||
|
var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino)
|
||||||
// If previously invalid part, remove from invalid list because it will be re-scanned
|
if (existingAudioFile) { // Audio file exists, sync paths
|
||||||
newParts.forEach((part) => {
|
if (existingAudiobook.syncAudioFile(existingAudioFile, file)) {
|
||||||
if (existingAudiobook.invalidParts.includes(part)) {
|
hasUpdatedAudioFiles = true
|
||||||
existingAudiobook.invalidParts = existingAudiobook.invalidParts.filter(p => p !== part)
|
}
|
||||||
|
} else {
|
||||||
|
newAudioFiles.push(file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Scan new audio parts found
|
if (newAudioFiles.length) {
|
||||||
await audioFileScanner.scanParts(existingAudiobook, newParts)
|
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) {
|
if (!existingAudiobook.tracks.length) {
|
||||||
@ -108,7 +160,7 @@ class Scanner {
|
|||||||
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
await this.db.removeEntity('audiobook', existingAudiobook.id)
|
||||||
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
|
||||||
} else {
|
} else {
|
||||||
var hasUpdates = removedAudioFiles.length || newParts.length
|
var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles
|
||||||
|
|
||||||
if (existingAudiobook.checkUpdateMissingParts()) {
|
if (existingAudiobook.checkUpdateMissingParts()) {
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
|
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
|
||||||
@ -119,6 +171,11 @@ class Scanner {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Syncs path and fullPath
|
||||||
|
if (existingAudiobook.syncPaths(audiobookData)) {
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
||||||
existingAudiobook.lastUpdate = Date.now()
|
existingAudiobook.lastUpdate = Date.now()
|
||||||
@ -129,12 +186,12 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
} // end if update existing
|
} // end if update existing
|
||||||
} else {
|
} else {
|
||||||
if (!audiobookData.parts.length) {
|
if (!audiobookData.audioFiles.length) {
|
||||||
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData)
|
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
|
||||||
} else {
|
} else {
|
||||||
var audiobook = new Audiobook()
|
var audiobook = new Audiobook()
|
||||||
audiobook.setData(audiobookData)
|
audiobook.setData(audiobookData)
|
||||||
await audioFileScanner.scanParts(audiobook, audiobookData.parts)
|
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
||||||
if (!audiobook.tracks.length) {
|
if (!audiobook.tracks.length) {
|
||||||
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
||||||
} else {
|
} else {
|
||||||
|
@ -18,9 +18,9 @@ class Server {
|
|||||||
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
||||||
this.Port = PORT
|
this.Port = PORT
|
||||||
this.Host = '0.0.0.0'
|
this.Host = '0.0.0.0'
|
||||||
this.ConfigPath = CONFIG_PATH
|
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
||||||
this.AudiobookPath = AUDIOBOOK_PATH
|
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
||||||
this.MetadataPath = METADATA_PATH
|
this.MetadataPath = Path.normalize(METADATA_PATH)
|
||||||
|
|
||||||
fs.ensureDirSync(CONFIG_PATH)
|
fs.ensureDirSync(CONFIG_PATH)
|
||||||
fs.ensureDirSync(METADATA_PATH)
|
fs.ensureDirSync(METADATA_PATH)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
var EventEmitter = require('events')
|
var EventEmitter = require('events')
|
||||||
var Logger = require('./Logger')
|
var Logger = require('./Logger')
|
||||||
var chokidar = require('chokidar')
|
var Watcher = require('watcher')
|
||||||
|
|
||||||
class FolderWatcher extends EventEmitter {
|
class FolderWatcher extends EventEmitter {
|
||||||
constructor(audiobookPath) {
|
constructor(audiobookPath) {
|
||||||
@ -12,15 +12,14 @@ class FolderWatcher extends EventEmitter {
|
|||||||
|
|
||||||
initWatcher() {
|
initWatcher() {
|
||||||
try {
|
try {
|
||||||
Logger.info('[WATCHER] Initializing..')
|
Logger.info('[FolderWatcher] Initializing..')
|
||||||
this.watcher = chokidar.watch(this.AudiobookPath, {
|
this.watcher = new Watcher(this.AudiobookPath, {
|
||||||
ignoreInitial: true,
|
|
||||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||||
persistent: true,
|
renameDetection: true,
|
||||||
awaitWriteFinish: {
|
renameTimeout: 2000,
|
||||||
stabilityThreshold: 2500,
|
recursive: true,
|
||||||
pollInterval: 500
|
ignoreInitial: true,
|
||||||
}
|
persistent: true
|
||||||
})
|
})
|
||||||
this.watcher
|
this.watcher
|
||||||
.on('add', (path) => {
|
.on('add', (path) => {
|
||||||
@ -29,10 +28,12 @@ class FolderWatcher extends EventEmitter {
|
|||||||
this.onFileUpdated(path)
|
this.onFileUpdated(path)
|
||||||
}).on('unlink', path => {
|
}).on('unlink', path => {
|
||||||
this.onFileRemoved(path)
|
this.onFileRemoved(path)
|
||||||
|
}).on('rename', (path, pathNext) => {
|
||||||
|
this.onRename(path, pathNext)
|
||||||
}).on('error', (error) => {
|
}).on('error', (error) => {
|
||||||
Logger.error(`Watcher error: ${error}`)
|
Logger.error(`[FolderWatcher] ${error}`)
|
||||||
}).on('ready', () => {
|
}).on('ready', () => {
|
||||||
Logger.info('[WATCHER] Ready')
|
Logger.info('[FolderWatcher] Ready')
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Chokidar watcher failed', error)
|
Logger.error('Chokidar watcher failed', error)
|
||||||
@ -53,7 +54,7 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFileRemoved(path) {
|
onFileRemoved(path) {
|
||||||
Logger.debug('FolderWatcher: File Removed', path)
|
Logger.debug('[FolderWatcher] File Removed', path)
|
||||||
this.emit('file_removed', {
|
this.emit('file_removed', {
|
||||||
path: path.replace(this.AudiobookPath, ''),
|
path: path.replace(this.AudiobookPath, ''),
|
||||||
fullPath: path
|
fullPath: path
|
||||||
@ -61,11 +62,15 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFileUpdated(path) {
|
onFileUpdated(path) {
|
||||||
Logger.debug('FolderWatcher: Updated File', path)
|
Logger.debug('[FolderWatcher] Updated File', path)
|
||||||
this.emit('file_updated', {
|
this.emit('file_updated', {
|
||||||
path: path.replace(this.AudiobookPath, ''),
|
path: path.replace(this.AudiobookPath, ''),
|
||||||
fullPath: path
|
fullPath: path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRename(pathFrom, pathTo) {
|
||||||
|
Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = FolderWatcher
|
module.exports = FolderWatcher
|
@ -26,6 +26,12 @@ class OpenLibrary {
|
|||||||
|
|
||||||
async getWorksData(worksKey) {
|
async getWorksData(worksKey) {
|
||||||
var worksData = await this.get(`${worksKey}.json`)
|
var worksData = await this.get(`${worksKey}.json`)
|
||||||
|
if (!worksData) {
|
||||||
|
return {
|
||||||
|
errorMsg: 'Works Data Request failed',
|
||||||
|
errorCode: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!worksData.covers) worksData.covers = []
|
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 coverImages = worksData.covers.filter(c => c > 0).map(c => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
||||||
var description = null
|
var description = null
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
var prober = require('./prober')
|
const prober = require('./prober')
|
||||||
|
const AudioFile = require('../AudioFile')
|
||||||
|
|
||||||
|
|
||||||
function getDefaultAudioStream(audioStreams) {
|
function getDefaultAudioStream(audioStreams) {
|
||||||
@ -76,41 +77,42 @@ function getTrackNumberFromFilename(filename) {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scanParts(audiobook, parts) {
|
async function scanAudioFiles(audiobook, newAudioFiles) {
|
||||||
if (!parts || !parts.length) {
|
if (!newAudioFiles || !newAudioFiles.length) {
|
||||||
Logger.error('[AudioFileScanner] Scan Parts', audiobook.title, 'No Parts', parts)
|
Logger.error('[AudioFileScanner] Scan Audio Files no files', audiobook.title)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var tracks = []
|
var tracks = []
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < newAudioFiles.length; i++) {
|
||||||
var fullPath = Path.join(audiobook.fullPath, parts[i])
|
var audioFile = newAudioFiles[i]
|
||||||
|
|
||||||
var scanData = await scan(fullPath)
|
var scanData = await scan(audioFile.fullPath)
|
||||||
if (!scanData || scanData.error) {
|
if (!scanData || scanData.error) {
|
||||||
Logger.error('[AudioFileScanner] Scan failed for', parts[i])
|
Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
|
||||||
audiobook.invalidParts.push(parts[i])
|
// audiobook.invalidAudioFiles.push(parts[i])
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
var trackNumFromMeta = getTrackNumberFromMeta(scanData)
|
||||||
var trackNumFromFilename = getTrackNumberFromFilename(parts[i])
|
var trackNumFromFilename = getTrackNumberFromFilename(audioFile.filename)
|
||||||
|
|
||||||
var audioFileObj = {
|
var audioFileObj = {
|
||||||
path: Path.join(audiobook.path, parts[i]),
|
ino: audioFile.ino,
|
||||||
ext: Path.extname(parts[i]),
|
filename: audioFile.filename,
|
||||||
filename: parts[i],
|
path: audioFile.path,
|
||||||
fullPath: fullPath,
|
fullPath: audioFile.fullPath,
|
||||||
|
ext: audioFile.ext,
|
||||||
...scanData,
|
...scanData,
|
||||||
trackNumFromMeta,
|
trackNumFromMeta,
|
||||||
trackNumFromFilename
|
trackNumFromFilename
|
||||||
}
|
}
|
||||||
audiobook.audioFiles.push(audioFileObj)
|
audiobook.addAudioFile(audioFileObj)
|
||||||
|
|
||||||
var trackNumber = 1
|
var trackNumber = 1
|
||||||
if (parts.length > 1) {
|
if (newAudioFiles.length > 1) {
|
||||||
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
|
||||||
if (trackNumber === null) {
|
if (trackNumber === null) {
|
||||||
Logger.error('[AudioFileScanner] Invalid track number for', parts[i])
|
Logger.error('[AudioFileScanner] Invalid track number for', audioFile.filename)
|
||||||
audioFileObj.invalid = true
|
audioFileObj.invalid = true
|
||||||
audioFileObj.error = 'Failed to get track number'
|
audioFileObj.error = 'Failed to get track number'
|
||||||
continue;
|
continue;
|
||||||
@ -118,7 +120,7 @@ async function scanParts(audiobook, parts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tracks.find(t => t.index === trackNumber)) {
|
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.invalid = true
|
||||||
audioFileObj.error = 'Duplicate track number'
|
audioFileObj.error = 'Duplicate track number'
|
||||||
continue;
|
continue;
|
||||||
@ -156,4 +158,4 @@ async function scanParts(audiobook, parts) {
|
|||||||
audiobook.tracks.sort((a, b) => a.index - b.index)
|
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 Path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
||||||
if (!caseSensitive) {
|
if (!caseSensitive) {
|
||||||
@ -48,24 +50,13 @@ module.exports.isObject = (val) => {
|
|||||||
return val !== null && typeof val === 'object'
|
return val !== null && typeof val === 'object'
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePath(path) {
|
module.exports.comparePaths = (path1, path2) => {
|
||||||
const replace = [
|
return path1 === path2 || Path.normalize(path1) === Path.normalize(path2)
|
||||||
[/\\/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) => {
|
module.exports.getIno = (path) => {
|
||||||
return (path1 === path2) || (normalizePath(path1) === normalizePath(path2))
|
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 Logger = require('../Logger')
|
||||||
const { cleanString } = require('./index')
|
const { cleanString } = require('./index')
|
||||||
|
|
||||||
const AUDIOBOOK_PARTS_FORMATS = ['m4b', 'mp3']
|
const AUDIO_FORMATS = ['m4b', 'mp3']
|
||||||
const INFO_FORMATS = ['nfo']
|
const INFO_FORMATS = ['nfo']
|
||||||
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp']
|
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp']
|
||||||
const EBOOK_FORMATS = ['epub', 'pdf']
|
const EBOOK_FORMATS = ['epub', 'pdf']
|
||||||
@ -23,7 +23,7 @@ function getPaths(path) {
|
|||||||
function getFileType(ext) {
|
function getFileType(ext) {
|
||||||
var ext_cleaned = ext.toLowerCase()
|
var ext_cleaned = ext.toLowerCase()
|
||||||
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
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 (INFO_FORMATS.includes(ext_cleaned)) return 'info'
|
||||||
if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image'
|
if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image'
|
||||||
if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook'
|
if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook'
|
||||||
@ -35,7 +35,7 @@ async function getAllAudiobookFiles(abRootPath) {
|
|||||||
var audiobooks = {}
|
var audiobooks = {}
|
||||||
|
|
||||||
paths.files.forEach((filepath) => {
|
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 pathformat = Path.parse(relpath)
|
||||||
var path = pathformat.dir
|
var path = pathformat.dir
|
||||||
|
|
||||||
@ -71,22 +71,20 @@ async function getAllAudiobookFiles(abRootPath) {
|
|||||||
publishYear: publishYear,
|
publishYear: publishYear,
|
||||||
path: path,
|
path: path,
|
||||||
fullPath: Path.join(abRootPath, path),
|
fullPath: Path.join(abRootPath, path),
|
||||||
parts: [],
|
audioFiles: [],
|
||||||
otherFiles: []
|
otherFiles: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var filetype = getFileType(pathformat.ext)
|
|
||||||
if (filetype === 'abpart') {
|
|
||||||
audiobooks[path].parts.push(pathformat.base)
|
|
||||||
} else {
|
|
||||||
var fileObj = {
|
var fileObj = {
|
||||||
filetype: filetype,
|
filetype: getFileType(pathformat.ext),
|
||||||
filename: pathformat.base,
|
filename: pathformat.base,
|
||||||
path: relpath,
|
path: relpath,
|
||||||
fullPath: filepath,
|
fullPath: filepath,
|
||||||
ext: pathformat.ext
|
ext: pathformat.ext
|
||||||
}
|
}
|
||||||
|
if (fileObj.filetype === 'audio') {
|
||||||
|
audiobooks[path].audioFiles.push(fileObj)
|
||||||
|
} else {
|
||||||
audiobooks[path].otherFiles.push(fileObj)
|
audiobooks[path].otherFiles.push(fileObj)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user