mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Always sync file inodes, save http url covers in cover directory
This commit is contained in:
		
							parent
							
								
									0db34dcab5
								
							
						
					
					
						commit
						8d9d5a8d1b
					
				@ -162,7 +162,11 @@ export default {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
        .catch((error) => {
 | 
					        .catch((error) => {
 | 
				
			||||||
          console.error('Failed', error)
 | 
					          console.error('Failed', error)
 | 
				
			||||||
          this.$toast.error('Oops, something went wrong...')
 | 
					          if (error.response && error.response.data) {
 | 
				
			||||||
 | 
					            this.$toast.error(error.response.data)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.$toast.error('Oops, something went wrong...')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          this.processingUpload = false
 | 
					          this.processingUpload = false
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -204,20 +208,39 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.isProcessing = true
 | 
					      this.isProcessing = true
 | 
				
			||||||
      const updatePayload = {
 | 
					      var success = false
 | 
				
			||||||
        book: {
 | 
					
 | 
				
			||||||
          cover: cover
 | 
					      // Download cover from url and use
 | 
				
			||||||
 | 
					      if (cover.startsWith('http:') || cover.startsWith('https:')) {
 | 
				
			||||||
 | 
					        success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, { url: cover }).catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed to download cover from url', error)
 | 
				
			||||||
 | 
					          if (error.response && error.response.data) {
 | 
				
			||||||
 | 
					            this.$toast.error(error.response.data)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Update local cover url
 | 
				
			||||||
 | 
					        const updatePayload = {
 | 
				
			||||||
 | 
					          book: {
 | 
				
			||||||
 | 
					            cover: cover
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed to update', error)
 | 
				
			||||||
 | 
					          if (error.response && error.response.data) {
 | 
				
			||||||
 | 
					            this.$toast.error(error.response.data)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => {
 | 
					      if (success) {
 | 
				
			||||||
        console.error('Failed to update', error)
 | 
					 | 
				
			||||||
        return false
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      this.isProcessing = false
 | 
					 | 
				
			||||||
      if (updatedAudiobook) {
 | 
					 | 
				
			||||||
        this.$toast.success('Update Successful')
 | 
					        this.$toast.success('Update Successful')
 | 
				
			||||||
        this.$emit('close')
 | 
					        this.$emit('close')
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.imageUrl = this.book.cover || ''
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.isProcessing = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    getSearchQuery() {
 | 
					    getSearchQuery() {
 | 
				
			||||||
      var searchQuery = `provider=openlibrary&title=${this.searchTitle}`
 | 
					      var searchQuery = `provider=openlibrary&title=${this.searchTitle}`
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      inputAccept: 'image/*'
 | 
					      inputAccept: '.png, .jpg, .jpeg, .webp'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {},
 | 
					  computed: {},
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf-client",
 | 
					  "name": "audiobookshelf-client",
 | 
				
			||||||
  "version": "1.3.2",
 | 
					  "version": "1.3.3",
 | 
				
			||||||
  "description": "Audiobook manager and player",
 | 
					  "description": "Audiobook manager and player",
 | 
				
			||||||
  "main": "index.js",
 | 
					  "main": "index.js",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 | 
				
			|||||||
@ -121,8 +121,8 @@ export default {
 | 
				
			|||||||
      author: null,
 | 
					      author: null,
 | 
				
			||||||
      series: null,
 | 
					      series: null,
 | 
				
			||||||
      acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac'],
 | 
					      acceptedAudioFormats: ['.mp3', '.m4b', '.m4a', '.flac'],
 | 
				
			||||||
      acceptedImageFormats: ['image/*'],
 | 
					      acceptedImageFormats: ['.png', '.jpg', '.jpeg', '.webp'],
 | 
				
			||||||
      inputAccept: 'image/*, .mp3, .m4b, .m4a, .flac',
 | 
					      inputAccept: '.png, .jpg, .jpeg, .webp, .mp3, .m4b, .m4a, .flac',
 | 
				
			||||||
      isDragOver: false,
 | 
					      isDragOver: false,
 | 
				
			||||||
      showUploader: true,
 | 
					      showUploader: true,
 | 
				
			||||||
      validAudioFiles: [],
 | 
					      validAudioFiles: [],
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										49
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										49
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf",
 | 
					  "name": "audiobookshelf",
 | 
				
			||||||
  "version": "1.2.7",
 | 
					  "version": "1.3.2",
 | 
				
			||||||
  "lockfileVersion": 1,
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
@ -573,6 +573,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew=="
 | 
					      "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "file-type": {
 | 
				
			||||||
 | 
					      "version": "10.11.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "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",
 | 
				
			||||||
@ -723,6 +728,14 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
 | 
					      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "image-type": {
 | 
				
			||||||
 | 
					      "version": "4.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "file-type": "^10.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "inflight": {
 | 
					    "inflight": {
 | 
				
			||||||
      "version": "1.0.6",
 | 
					      "version": "1.0.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
 | 
				
			||||||
@ -1032,6 +1045,16 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
 | 
					      "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "p-finally": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "p-try": {
 | 
				
			||||||
 | 
					      "version": "2.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "parseurl": {
 | 
					    "parseurl": {
 | 
				
			||||||
      "version": "1.3.3",
 | 
					      "version": "1.3.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 | 
				
			||||||
@ -1047,6 +1070,11 @@
 | 
				
			|||||||
      "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="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "pify": {
 | 
				
			||||||
 | 
					      "version": "4.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "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",
 | 
				
			||||||
@ -1124,6 +1152,15 @@
 | 
				
			|||||||
        "unpipe": "1.0.0"
 | 
					        "unpipe": "1.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "read-chunk": {
 | 
				
			||||||
 | 
					      "version": "3.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ZdiZJXXoZYE08SzZvTipHhI+ZW0FpzxmFtLI3vIeMuRN9ySbIZ+SZawKogqJ7dxW9fJ/W73BNtxu4Zu/bZp+Ng==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "pify": "^4.0.1",
 | 
				
			||||||
 | 
					        "with-open-file": "^0.1.5"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "readable-stream": {
 | 
					    "readable-stream": {
 | 
				
			||||||
      "version": "3.6.0",
 | 
					      "version": "3.6.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
 | 
				
			||||||
@ -1424,6 +1461,16 @@
 | 
				
			|||||||
        "isexe": "^2.0.0"
 | 
					        "isexe": "^2.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "with-open-file": {
 | 
				
			||||||
 | 
					      "version": "0.1.7",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "p-finally": "^1.0.0",
 | 
				
			||||||
 | 
					        "p-try": "^2.1.0",
 | 
				
			||||||
 | 
					        "pify": "^4.0.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "wrappy": {
 | 
					    "wrappy": {
 | 
				
			||||||
      "version": "1.0.2",
 | 
					      "version": "1.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf",
 | 
					  "name": "audiobookshelf",
 | 
				
			||||||
  "version": "1.3.2",
 | 
					  "version": "1.3.3",
 | 
				
			||||||
  "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": {
 | 
				
			||||||
@ -32,12 +32,14 @@
 | 
				
			|||||||
    "express-rate-limit": "^5.3.0",
 | 
					    "express-rate-limit": "^5.3.0",
 | 
				
			||||||
    "fluent-ffmpeg": "^2.1.2",
 | 
					    "fluent-ffmpeg": "^2.1.2",
 | 
				
			||||||
    "fs-extra": "^10.0.0",
 | 
					    "fs-extra": "^10.0.0",
 | 
				
			||||||
 | 
					    "image-type": "^4.1.0",
 | 
				
			||||||
    "ip": "^1.1.5",
 | 
					    "ip": "^1.1.5",
 | 
				
			||||||
    "jsonwebtoken": "^8.5.1",
 | 
					    "jsonwebtoken": "^8.5.1",
 | 
				
			||||||
    "libgen": "^2.1.0",
 | 
					    "libgen": "^2.1.0",
 | 
				
			||||||
    "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",
 | 
				
			||||||
 | 
					    "read-chunk": "^3.1.0",
 | 
				
			||||||
    "socket.io": "^4.1.3",
 | 
					    "socket.io": "^4.1.3",
 | 
				
			||||||
    "watcher": "^1.2.0"
 | 
					    "watcher": "^1.2.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -3,17 +3,17 @@ const Path = require('path')
 | 
				
			|||||||
const fs = require('fs-extra')
 | 
					const fs = require('fs-extra')
 | 
				
			||||||
const Logger = require('./Logger')
 | 
					const Logger = require('./Logger')
 | 
				
			||||||
const User = require('./objects/User')
 | 
					const User = require('./objects/User')
 | 
				
			||||||
const { isObject, isAcceptableCoverMimeType } = require('./utils/index')
 | 
					const { isObject } = require('./utils/index')
 | 
				
			||||||
const { CoverDestination } = require('./utils/constants')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApiController {
 | 
					class ApiController {
 | 
				
			||||||
  constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, emitter, clientEmitter) {
 | 
					  constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, emitter, clientEmitter) {
 | 
				
			||||||
    this.db = db
 | 
					    this.db = db
 | 
				
			||||||
    this.scanner = scanner
 | 
					    this.scanner = scanner
 | 
				
			||||||
    this.auth = auth
 | 
					    this.auth = auth
 | 
				
			||||||
    this.streamManager = streamManager
 | 
					    this.streamManager = streamManager
 | 
				
			||||||
    this.rssFeeds = rssFeeds
 | 
					    this.rssFeeds = rssFeeds
 | 
				
			||||||
    this.downloadManager = downloadManager
 | 
					    this.downloadManager = downloadManager
 | 
				
			||||||
 | 
					    this.coverController = coverController
 | 
				
			||||||
    this.emitter = emitter
 | 
					    this.emitter = emitter
 | 
				
			||||||
    this.clientEmitter = clientEmitter
 | 
					    this.clientEmitter = clientEmitter
 | 
				
			||||||
    this.MetadataPath = MetadataPath
 | 
					    this.MetadataPath = MetadataPath
 | 
				
			||||||
@ -221,77 +221,36 @@ class ApiController {
 | 
				
			|||||||
      Logger.warn('User attempted to upload a cover without permission', req.user)
 | 
					      Logger.warn('User attempted to upload a cover without permission', req.user)
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!req.files || !req.files.cover) {
 | 
					
 | 
				
			||||||
      return res.status(400).send('No files were uploaded')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var audiobookId = req.params.id
 | 
					    var audiobookId = req.params.id
 | 
				
			||||||
    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
 | 
					    var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
 | 
				
			||||||
    if (!audiobook) {
 | 
					    if (!audiobook) {
 | 
				
			||||||
      return res.status(404).send('Audiobook not found')
 | 
					      return res.status(404).send('Audiobook not found')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var coverFile = req.files.cover
 | 
					    var result = null
 | 
				
			||||||
    var mimeType = coverFile.mimetype
 | 
					    if (req.body && req.body.url) {
 | 
				
			||||||
    var extname = Path.extname(coverFile.name.toLowerCase()) || '.jpg'
 | 
					      Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`)
 | 
				
			||||||
    if (!isAcceptableCoverMimeType(mimeType)) {
 | 
					      result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url)
 | 
				
			||||||
      return res.status(400).send('Invalid image file type: ' + mimeType)
 | 
					    } else if (req.files && req.files.cover) {
 | 
				
			||||||
    }
 | 
					      Logger.debug(`[ApiController] Handling uploaded cover`)
 | 
				
			||||||
 | 
					      var coverFile = req.files.cover
 | 
				
			||||||
    var coverDestination = this.db.serverSettings ? this.db.serverSettings.coverDestination : CoverDestination.METADATA
 | 
					      result = await this.coverController.uploadCover(audiobook, coverFile)
 | 
				
			||||||
    Logger.info(`[ApiController] Cover Upload destination ${coverDestination}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var coverDirpath = audiobook.fullPath
 | 
					 | 
				
			||||||
    var coverRelDirpath = Path.join('/local', audiobook.path)
 | 
					 | 
				
			||||||
    if (coverDestination === CoverDestination.METADATA) {
 | 
					 | 
				
			||||||
      coverDirpath = Path.join(this.MetadataPath, 'books', audiobookId)
 | 
					 | 
				
			||||||
      coverRelDirpath = Path.join('/metadata', 'books', audiobookId)
 | 
					 | 
				
			||||||
      Logger.debug(`[ApiController] storing in metadata | ${coverDirpath}`)
 | 
					 | 
				
			||||||
      await fs.ensureDir(coverDirpath)
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      Logger.debug(`[ApiController] storing in audiobook | ${coverRelDirpath}`)
 | 
					      return res.status(400).send('Invalid request no file or url')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var coverFilename = `cover${extname}`
 | 
					    if (result && result.error) {
 | 
				
			||||||
    var coverFullPath = Path.join(coverDirpath, coverFilename)
 | 
					      return res.status(400).send(result.error)
 | 
				
			||||||
    var coverPath = Path.join(coverRelDirpath, coverFilename)
 | 
					    } else if (!result || !result.cover) {
 | 
				
			||||||
 | 
					      return res.status(500).send('Unknown error occurred')
 | 
				
			||||||
    // If current cover is a metadata cover and does not match replacement, then remove it
 | 
					 | 
				
			||||||
    var currentBookCover = audiobook.book.cover
 | 
					 | 
				
			||||||
    if (currentBookCover && currentBookCover.startsWith(Path.sep + 'metadata')) {
 | 
					 | 
				
			||||||
      Logger.debug(`Current Book Cover is metadata ${currentBookCover}`)
 | 
					 | 
				
			||||||
      if (currentBookCover !== coverPath) {
 | 
					 | 
				
			||||||
        Logger.info(`[ApiController] removing old metadata cover "${currentBookCover}"`)
 | 
					 | 
				
			||||||
        var oldFullBookCoverPath = Path.join(this.MetadataPath, currentBookCover.replace(Path.sep + 'metadata', ''))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Metadata path may have changed, check if exists first
 | 
					 | 
				
			||||||
        var exists = await fs.pathExists(oldFullBookCoverPath)
 | 
					 | 
				
			||||||
        if (exists) {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await fs.remove(oldFullBookCoverPath)
 | 
					 | 
				
			||||||
          } catch (error) {
 | 
					 | 
				
			||||||
            Logger.error(`[ApiController] Failed to remove old metadata book cover ${oldFullBookCoverPath}`)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
 | 
					 | 
				
			||||||
      Logger.error('Failed to move cover file', path, error)
 | 
					 | 
				
			||||||
      return false
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!success) {
 | 
					 | 
				
			||||||
      return res.status(500).send('Failed to move cover into destination')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Logger.info(`[ApiController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    audiobook.updateBookCover(coverPath)
 | 
					 | 
				
			||||||
    await this.db.updateAudiobook(audiobook)
 | 
					    await this.db.updateAudiobook(audiobook)
 | 
				
			||||||
    this.emitter('audiobook_updated', audiobook.toJSONMinified())
 | 
					    this.emitter('audiobook_updated', audiobook.toJSONMinified())
 | 
				
			||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
      success: true,
 | 
					      success: true,
 | 
				
			||||||
      cover: coverPath
 | 
					      cover: result.cover
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										193
									
								
								server/CoverController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								server/CoverController.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					const fs = require('fs-extra')
 | 
				
			||||||
 | 
					const Path = require('path')
 | 
				
			||||||
 | 
					const axios = require('axios')
 | 
				
			||||||
 | 
					const Logger = require('./Logger')
 | 
				
			||||||
 | 
					const readChunk = require('read-chunk')
 | 
				
			||||||
 | 
					const imageType = require('image-type')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const globals = require('./utils/globals')
 | 
				
			||||||
 | 
					const { CoverDestination } = require('./utils/constants')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CoverController {
 | 
				
			||||||
 | 
					  constructor(db, MetadataPath, AudiobookPath) {
 | 
				
			||||||
 | 
					    this.db = db
 | 
				
			||||||
 | 
					    this.MetadataPath = MetadataPath
 | 
				
			||||||
 | 
					    this.BookMetadataPath = Path.join(this.MetadataPath, 'books')
 | 
				
			||||||
 | 
					    this.AudiobookPath = AudiobookPath
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getCoverDirectory(audiobook) {
 | 
				
			||||||
 | 
					    if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        fullPath: audiobook.fullPath,
 | 
				
			||||||
 | 
					        relPath: Path.join('/local', audiobook.path)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        fullPath: Path.join(this.BookMetadataPath, audiobook.id),
 | 
				
			||||||
 | 
					        relPath: Path.join('/metadata', 'books', audiobook.id)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getFilesInDirectory(dir) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return fs.readdir(dir)
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`[CoverController] Failed to get files in dir ${dir}`, error)
 | 
				
			||||||
 | 
					      return []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  removeFile(filepath) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return fs.pathExists(filepath).then((exists) => {
 | 
				
			||||||
 | 
					        if (!exists) Logger.warn(`[CoverController] Attempting to remove file that does not exist ${filepath}`)
 | 
				
			||||||
 | 
					        return exists ? fs.unlink(filepath) : false
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`[CoverController] Failed to remove file "${filepath}"`, error)
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove covers in metadata/books/{ID} that dont have the same filename as the new cover
 | 
				
			||||||
 | 
					  async checkBookMetadataCovers(dirpath, newCoverExt) {
 | 
				
			||||||
 | 
					    var filesInDir = await this.getFilesInDirectory(dirpath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let i = 0; i < filesInDir.length; i++) {
 | 
				
			||||||
 | 
					      var file = filesInDir[i]
 | 
				
			||||||
 | 
					      var _extname = Path.extname(file)
 | 
				
			||||||
 | 
					      var _filename = Path.basename(file, _extname)
 | 
				
			||||||
 | 
					      if (_filename === 'cover' && _extname !== newCoverExt) {
 | 
				
			||||||
 | 
					        var filepath = Path.join(dirpath, file)
 | 
				
			||||||
 | 
					        Logger.debug(`[CoverController] Removing old cover from metadata "${filepath}"`)
 | 
				
			||||||
 | 
					        await this.removeFile(filepath)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async checkFileIsValidImage(imagepath) {
 | 
				
			||||||
 | 
					    const buffer = await readChunk(imagepath, 0, 12)
 | 
				
			||||||
 | 
					    const imgType = imageType(buffer)
 | 
				
			||||||
 | 
					    if (!imgType) {
 | 
				
			||||||
 | 
					      await this.removeFile(imagepath)
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        error: 'Invalid image'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!globals.SupportedImageTypes.includes(imgType.ext)) {
 | 
				
			||||||
 | 
					      await this.removeFile(imagepath)
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        error: `Invalid image type ${imgType.ext} (Supported: ${globals.SupportedImageTypes.join(',')})`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return imgType
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async uploadCover(audiobook, coverFile) {
 | 
				
			||||||
 | 
					    var extname = Path.extname(coverFile.name.toLowerCase())
 | 
				
			||||||
 | 
					    if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var { fullPath, relPath } = this.getCoverDirectory(audiobook)
 | 
				
			||||||
 | 
					    await fs.ensureDir(fullPath)
 | 
				
			||||||
 | 
					    var isStoringInMetadata = relPath.slice(1).startsWith('metadata')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var coverFilename = `cover${extname}`
 | 
				
			||||||
 | 
					    var coverFullPath = Path.join(fullPath, coverFilename)
 | 
				
			||||||
 | 
					    var coverPath = Path.join(relPath, coverFilename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isStoringInMetadata) {
 | 
				
			||||||
 | 
					      await this.checkBookMetadataCovers(fullPath, extname)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Move cover from temp upload dir to destination
 | 
				
			||||||
 | 
					    var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
 | 
				
			||||||
 | 
					      Logger.error('[CoverController] Failed to move cover file', path, error)
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!success) {
 | 
				
			||||||
 | 
					      // return res.status(500).send('Failed to move cover into destination')
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        error: 'Failed to move cover into destination'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    audiobook.updateBookCover(coverPath)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      cover: coverPath
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async downloadFile(url, filepath) {
 | 
				
			||||||
 | 
					    Logger.debug(`[CoverController] Starting file download to ${filepath}`)
 | 
				
			||||||
 | 
					    const writer = fs.createWriteStream(filepath)
 | 
				
			||||||
 | 
					    const response = await axios({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'GET',
 | 
				
			||||||
 | 
					      responseType: 'stream'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    response.data.pipe(writer)
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      writer.on('finish', resolve)
 | 
				
			||||||
 | 
					      writer.on('error', reject)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async downloadCoverFromUrl(audiobook, url) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      var { fullPath, relPath } = this.getCoverDirectory(audiobook)
 | 
				
			||||||
 | 
					      await fs.ensureDir(fullPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var temppath = Path.join(fullPath, 'cover')
 | 
				
			||||||
 | 
					      var success = await this.downloadFile(url, temppath).then(() => true).catch((err) => {
 | 
				
			||||||
 | 
					        Logger.error(`[CoverController] Download image file failed for "${url}"`, err)
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      if (!success) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          error: 'Failed to download image from url'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var imgtype = await this.checkFileIsValidImage(temppath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (imgtype.error) {
 | 
				
			||||||
 | 
					        return imgtype
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var coverFilename = `cover.${imgtype.ext}`
 | 
				
			||||||
 | 
					      var coverPath = Path.join(relPath, coverFilename)
 | 
				
			||||||
 | 
					      var coverFullPath = Path.join(fullPath, coverFilename)
 | 
				
			||||||
 | 
					      await fs.rename(temppath, coverFullPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var isStoringInMetadata = relPath.slice(1).startsWith('metadata')
 | 
				
			||||||
 | 
					      if (isStoringInMetadata) {
 | 
				
			||||||
 | 
					        await this.checkBookMetadataCovers(fullPath, '.' + imgtype.ext)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      audiobook.updateBookCover(coverPath)
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        cover: coverPath
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`[CoverController] Fetch cover image from url "${url}" failed`, error)
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        error: 'Failed to fetch image from url'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = CoverController
 | 
				
			||||||
@ -106,14 +106,19 @@ class Scanner {
 | 
				
			|||||||
      // check an audiobook exists with matching path, then update inodes
 | 
					      // check an audiobook exists with matching path, then update inodes
 | 
				
			||||||
      existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path)
 | 
					      existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path)
 | 
				
			||||||
      if (existingAudiobook) {
 | 
					      if (existingAudiobook) {
 | 
				
			||||||
 | 
					        existingAudiobook.ino = audiobookData.ino
 | 
				
			||||||
        hasUpdatedIno = true
 | 
					        hasUpdatedIno = true
 | 
				
			||||||
        var filesUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData)
 | 
					 | 
				
			||||||
        Logger.info(`[Scanner] Updating inode value for "${existingAudiobook.title}" - ${filesUpdated} files updated`)
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`)
 | 
					 | 
				
			||||||
    if (existingAudiobook) {
 | 
					    if (existingAudiobook) {
 | 
				
			||||||
 | 
					      // Always sync files and inode values
 | 
				
			||||||
 | 
					      var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData)
 | 
				
			||||||
 | 
					      if (hasUpdatedIno || filesInodeUpdated > 0) {
 | 
				
			||||||
 | 
					        Logger.info(`[Scanner] Updating inode value for "${existingAudiobook.title}" - ${filesInodeUpdated} files updated`)
 | 
				
			||||||
 | 
					        hasUpdatedIno = true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // TEMP: Check if is older audiobook and needs force rescan
 | 
					      // TEMP: Check if is older audiobook and needs force rescan
 | 
				
			||||||
      if (!forceAudioFileScan && existingAudiobook.checkNeedsAudioFileRescan()) {
 | 
					      if (!forceAudioFileScan && existingAudiobook.checkNeedsAudioFileRescan()) {
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ const HlsController = require('./HlsController')
 | 
				
			|||||||
const StreamManager = require('./StreamManager')
 | 
					const StreamManager = require('./StreamManager')
 | 
				
			||||||
const RssFeeds = require('./RssFeeds')
 | 
					const RssFeeds = require('./RssFeeds')
 | 
				
			||||||
const DownloadManager = require('./DownloadManager')
 | 
					const DownloadManager = require('./DownloadManager')
 | 
				
			||||||
 | 
					const CoverController = require('./CoverController')
 | 
				
			||||||
// const EbookReader = require('./EbookReader')
 | 
					// const EbookReader = require('./EbookReader')
 | 
				
			||||||
const Logger = require('./Logger')
 | 
					const Logger = require('./Logger')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,9 +39,11 @@ class Server {
 | 
				
			|||||||
    this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this))
 | 
					    this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this))
 | 
				
			||||||
    this.streamManager = new StreamManager(this.db, this.MetadataPath)
 | 
					    this.streamManager = new StreamManager(this.db, this.MetadataPath)
 | 
				
			||||||
    this.rssFeeds = new RssFeeds(this.Port, this.db)
 | 
					    this.rssFeeds = new RssFeeds(this.Port, this.db)
 | 
				
			||||||
 | 
					    this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath)
 | 
				
			||||||
    this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
 | 
					    this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
 | 
				
			||||||
    this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
					    this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
				
			||||||
    this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
 | 
					    this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // this.ebookReader = new EbookReader(this.db, this.MetadataPath, this.AudiobookPath)
 | 
					    // this.ebookReader = new EbookReader(this.db, this.MetadataPath, this.AudiobookPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.server = null
 | 
					    this.server = null
 | 
				
			||||||
@ -132,6 +135,33 @@ class Server {
 | 
				
			|||||||
    socket.emit('save_metadata_complete', response)
 | 
					    socket.emit('save_metadata_complete', response)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Remove unused /metadata/books/{id} folders
 | 
				
			||||||
 | 
					  async purgeMetadata() {
 | 
				
			||||||
 | 
					    var booksMetadata = Path.join(this.MetadataPath, 'books')
 | 
				
			||||||
 | 
					    var booksMetadataExists = await fs.pathExists(booksMetadata)
 | 
				
			||||||
 | 
					    if (!booksMetadataExists) return
 | 
				
			||||||
 | 
					    var foldersInBooksMetadata = await fs.readdir(booksMetadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var purged = 0
 | 
				
			||||||
 | 
					    await Promise.all(foldersInBooksMetadata.map(async foldername => {
 | 
				
			||||||
 | 
					      var hasMatchingAudiobook = this.audiobooks.find(ab => ab.id === foldername)
 | 
				
			||||||
 | 
					      if (!hasMatchingAudiobook) {
 | 
				
			||||||
 | 
					        var folderPath = Path.join(booksMetadata, foldername)
 | 
				
			||||||
 | 
					        Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await fs.remove(folderPath).then(() => {
 | 
				
			||||||
 | 
					          purged++
 | 
				
			||||||
 | 
					        }).catch((err) => {
 | 
				
			||||||
 | 
					          Logger.error(`[Server] Failed to delete folder path ${folderPath}`, err)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					    if (purged > 0) {
 | 
				
			||||||
 | 
					      Logger.info(`[Server] Purged ${purged} unused audiobook metadata`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return purged
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
    Logger.info('[Server] Init')
 | 
					    Logger.info('[Server] Init')
 | 
				
			||||||
    await this.streamManager.ensureStreamsDir()
 | 
					    await this.streamManager.ensureStreamsDir()
 | 
				
			||||||
@ -141,6 +171,8 @@ class Server {
 | 
				
			|||||||
    await this.db.init()
 | 
					    await this.db.init()
 | 
				
			||||||
    this.auth.init()
 | 
					    this.auth.init()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.purgeMetadata()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.watcher.initWatcher()
 | 
					    this.watcher.initWatcher()
 | 
				
			||||||
    this.watcher.on('files', this.filesChanged.bind(this))
 | 
					    this.watcher.on('files', this.filesChanged.bind(this))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,7 @@ class AudioTrack {
 | 
				
			|||||||
      size: this.size,
 | 
					      size: this.size,
 | 
				
			||||||
      bitRate: this.bitRate,
 | 
					      bitRate: this.bitRate,
 | 
				
			||||||
      language: this.language,
 | 
					      language: this.language,
 | 
				
			||||||
 | 
					      codec: this.codec,
 | 
				
			||||||
      timeBase: this.timeBase,
 | 
					      timeBase: this.timeBase,
 | 
				
			||||||
      channels: this.channels,
 | 
					      channels: this.channels,
 | 
				
			||||||
      channelLayout: this.channelLayout,
 | 
					      channelLayout: this.channelLayout,
 | 
				
			||||||
@ -82,7 +83,7 @@ class AudioTrack {
 | 
				
			|||||||
    this.size = probeData.size
 | 
					    this.size = probeData.size
 | 
				
			||||||
    this.bitRate = probeData.bitRate
 | 
					    this.bitRate = probeData.bitRate
 | 
				
			||||||
    this.language = probeData.language
 | 
					    this.language = probeData.language
 | 
				
			||||||
    this.codec = probeData.codec
 | 
					    this.codec = probeData.codec || null
 | 
				
			||||||
    this.timeBase = probeData.timeBase
 | 
					    this.timeBase = probeData.timeBase
 | 
				
			||||||
    this.channels = probeData.channels
 | 
					    this.channels = probeData.channels
 | 
				
			||||||
    this.channelLayout = probeData.channelLayout
 | 
					    this.channelLayout = probeData.channelLayout
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								server/utils/globals.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/utils/globals.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					const globals = {
 | 
				
			||||||
 | 
					  SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
 | 
				
			||||||
 | 
					  SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac'],
 | 
				
			||||||
 | 
					  SupportedEbookTypes: ['epub', 'pdf', 'mobi']
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = globals
 | 
				
			||||||
@ -63,7 +63,3 @@ module.exports.getIno = (path) => {
 | 
				
			|||||||
    return null
 | 
					    return null
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports.isAcceptableCoverMimeType = (mimeType) => {
 | 
					 | 
				
			||||||
  return mimeType && mimeType.startsWith('image/')
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -2,11 +2,7 @@ const Path = require('path')
 | 
				
			|||||||
const dir = require('node-dir')
 | 
					const dir = require('node-dir')
 | 
				
			||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
const { getIno } = require('./index')
 | 
					const { getIno } = require('./index')
 | 
				
			||||||
 | 
					const globals = require('./globals')
 | 
				
			||||||
const AUDIO_FORMATS = ['m4b', 'mp3', 'm4a', 'flac']
 | 
					 | 
				
			||||||
const INFO_FORMATS = ['nfo']
 | 
					 | 
				
			||||||
const IMAGE_FORMATS = ['png', 'jpg', 'jpeg', 'webp']
 | 
					 | 
				
			||||||
const EBOOK_FORMATS = ['epub', 'pdf', 'mobi']
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getPaths(path) {
 | 
					function getPaths(path) {
 | 
				
			||||||
  return new Promise((resolve) => {
 | 
					  return new Promise((resolve) => {
 | 
				
			||||||
@ -24,7 +20,7 @@ function isAudioFile(path) {
 | 
				
			|||||||
  if (!path) return false
 | 
					  if (!path) return false
 | 
				
			||||||
  var ext = Path.extname(path)
 | 
					  var ext = Path.extname(path)
 | 
				
			||||||
  if (!ext) return false
 | 
					  if (!ext) return false
 | 
				
			||||||
  return AUDIO_FORMATS.includes(ext.slice(1).toLowerCase())
 | 
					  return globals.SupportedAudioTypes.includes(ext.slice(1).toLowerCase())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) {
 | 
					function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) {
 | 
				
			||||||
@ -107,10 +103,10 @@ function cleanFileObjects(basepath, abrelpath, files) {
 | 
				
			|||||||
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 (AUDIO_FORMATS.includes(ext_cleaned)) return 'audio'
 | 
					  if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
 | 
				
			||||||
  if (INFO_FORMATS.includes(ext_cleaned)) return 'info'
 | 
					  if (ext_cleaned === 'nfo') return 'info'
 | 
				
			||||||
  if (IMAGE_FORMATS.includes(ext_cleaned)) return 'image'
 | 
					  if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
 | 
				
			||||||
  if (EBOOK_FORMATS.includes(ext_cleaned)) return 'ebook'
 | 
					  if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
 | 
				
			||||||
  return 'unknown'
 | 
					  return 'unknown'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user