From 63cae5b0ed0f357778622acdff5792377df5b9db Mon Sep 17 00:00:00 2001 From: Mark Cooper Date: Wed, 25 Aug 2021 17:36:54 -0500 Subject: [PATCH] Adding inode to files and audiobooks to support renaming, setting up watcher and removing chokidar --- client/package.json | 2 +- client/tailwind.config.js | 3 +- package-lock.json | 166 ++++++++++++------------------- package.json | 6 +- server/AudioFile.js | 135 +++++++++++++++++++++++++ server/AudioTrack.js | 32 +++++- server/Audiobook.js | 130 +++++++++++++++++++----- server/AudiobookFile.js | 48 +++++++++ server/Book.js | 12 +++ server/Logger.js | 5 + server/Scanner.js | 115 +++++++++++++++------ server/Server.js | 6 +- server/Watcher.js | 31 +++--- server/providers/OpenLibrary.js | 6 ++ server/utils/audioFileScanner.js | 40 ++++---- server/utils/index.js | 27 ++--- server/utils/scandir.js | 28 +++--- 17 files changed, 558 insertions(+), 234 deletions(-) create mode 100644 server/AudioFile.js create mode 100644 server/AudiobookFile.js diff --git a/client/package.json b/client/package.json index a882616f..74fa3690 100644 --- a/client/package.json +++ b/client/package.json @@ -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": { diff --git a/client/tailwind.config.js b/client/tailwind.config.js index a5272496..41643a0b 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -4,7 +4,8 @@ module.exports = { purge: { options: { safelist: [ - 'bg-success' + 'bg-success', + 'bg-red-600' ] } }, diff --git a/package-lock.json b/package-lock.json index 9d13d3a4..cb88dd9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 211b5b05..fe5728f5 100644 --- a/package.json +++ b/package.json @@ -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": {} } \ No newline at end of file diff --git a/server/AudioFile.js b/server/AudioFile.js new file mode 100644 index 00000000..3ac55694 --- /dev/null +++ b/server/AudioFile.js @@ -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 \ No newline at end of file diff --git a/server/AudioTrack.js b/server/AudioTrack.js index 12fc041b..2e32cc43 100644 --- a/server/AudioTrack.js +++ b/server/AudioTrack.js @@ -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 \ No newline at end of file diff --git a/server/Audiobook.js b/server/Audiobook.js index b99762cc..169dcaf2 100644 --- a/server/Audiobook.js +++ b/server/Audiobook.js @@ -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 \ No newline at end of file diff --git a/server/AudiobookFile.js b/server/AudiobookFile.js new file mode 100644 index 00000000..5ac108ff --- /dev/null +++ b/server/AudiobookFile.js @@ -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 \ No newline at end of file diff --git a/server/Book.js b/server/Book.js index 14619853..601e0221 100644 --- a/server/Book.js +++ b/server/Book.js @@ -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) } diff --git a/server/Logger.js b/server/Logger.js index f4960154..0ca5f0f1 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -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) diff --git a/server/Scanner.js b/server/Scanner.js index 7f9ee98a..0d844ae9 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -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 { diff --git a/server/Server.js b/server/Server.js index d2a91e65..9761b00a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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) diff --git a/server/Watcher.js b/server/Watcher.js index 740f04a2..68ec2038 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -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 \ No newline at end of file diff --git a/server/providers/OpenLibrary.js b/server/providers/OpenLibrary.js index ca4a1abb..284d9477 100644 --- a/server/providers/OpenLibrary.js +++ b/server/providers/OpenLibrary.js @@ -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 diff --git a/server/utils/audioFileScanner.js b/server/utils/audioFileScanner.js index 674182b3..f4cfaf49 100644 --- a/server/utils/audioFileScanner.js +++ b/server/utils/audioFileScanner.js @@ -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 \ No newline at end of file +module.exports.scanAudioFiles = scanAudioFiles \ No newline at end of file diff --git a/server/utils/index.js b/server/utils/index.js index c41ee0ac..8b14a6e5 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -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 + }) } \ No newline at end of file diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 4cb1537b..ae7b9ce8 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -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) } })