diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 274affa722..8bf0c31ec5 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -329,6 +329,7 @@ class MiscController { await libraryItem.media.update({ tags: libraryItem.media.tags }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -370,6 +371,7 @@ class MiscController { await libraryItem.media.update({ tags: libraryItem.media.tags }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -462,6 +464,7 @@ class MiscController { await libraryItem.media.update({ genres: libraryItem.media.genres }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ @@ -503,6 +506,7 @@ class MiscController { await libraryItem.media.update({ genres: libraryItem.media.genres }) + await libraryItem.saveMetadataFile() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) numItemsUpdated++ diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 704e2f105f..5a35a5d6a5 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,8 +1,12 @@ +const Path = require('path') const { DataTypes, Model } = require('sequelize') +const fsExtra = require('../libs/fsExtra') const Logger = require('../Logger') const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') const { areEquivalent } = require('../utils/index') +const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') +const LibraryFile = require('../objects/files/LibraryFile') const Book = require('./Book') const Podcast = require('./Podcast') @@ -828,6 +832,147 @@ class LibraryItem extends Model { return this[mixinMethodName](options) } + /** + * + * @returns {Promise} + */ + getMediaExpanded() { + if (this.mediaType === 'podcast') { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + }) + } else { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [ + [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ] + }) + } + } + + /** + * + * @returns {Promise} + */ + async saveMetadataFile() { + let metadataPath = Path.join(global.MetadataPath, 'items', this.id) + let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem + if (storeMetadataWithItem && !this.isFile) { + metadataPath = this.path + } else { + // Make sure metadata book dir exists + storeMetadataWithItem = false + await fsExtra.ensureDir(metadataPath) + } + + const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + + // Expanded with series, authors, podcastEpisodes + const mediaExpanded = this.media || await this.getMediaExpanded() + + let jsonObject = {} + if (this.mediaType === 'book') { + jsonObject = { + tags: mediaExpanded.tags || [], + chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [], + title: mediaExpanded.title, + subtitle: mediaExpanded.subtitle, + authors: mediaExpanded.authors.map(a => a.name), + narrators: mediaExpanded.narrators, + series: mediaExpanded.series.map(se => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }), + genres: mediaExpanded.genres || [], + publishedYear: mediaExpanded.publishedYear, + publishedDate: mediaExpanded.publishedDate, + publisher: mediaExpanded.publisher, + description: mediaExpanded.description, + isbn: mediaExpanded.isbn, + asin: mediaExpanded.asin, + language: mediaExpanded.language, + explicit: !!mediaExpanded.explicit, + abridged: !!mediaExpanded.abridged + } + } else { + jsonObject = { + tags: mediaExpanded.tags || [], + title: mediaExpanded.title, + author: mediaExpanded.author, + description: mediaExpanded.description, + releaseDate: mediaExpanded.releaseDate, + genres: mediaExpanded.genres || [], + feedURL: mediaExpanded.feedURL, + imageURL: mediaExpanded.imageURL, + itunesPageURL: mediaExpanded.itunesPageURL, + itunesId: mediaExpanded.itunesId, + itunesArtistId: mediaExpanded.itunesArtistId, + asin: mediaExpanded.asin, + language: mediaExpanded.language, + explicit: !!mediaExpanded.explicit, + podcastType: mediaExpanded.podcastType + } + } + + + return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + this.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) + if (libraryItemDirTimestamps) { + this.mtime = libraryItemDirTimestamps.mtimeMs + this.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + this.size = size + await this.save() + } + } + + Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`) + + return metadataLibraryFile + }).catch((error) => { + Logger.error(`Failed to save json file at "${metadataFilePath}"`, error) + return null + }) + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js index fbc6186e05..677b11c7e6 100644 --- a/server/utils/queries/libraryItemFilters.js +++ b/server/utils/queries/libraryItemFilters.js @@ -34,6 +34,10 @@ module.exports = { attributes: ['sequence'] } } + ], + order: [ + [Database.authorModel, Database.bookAuthorModel, 'createdAt', 'ASC'], + [Database.seriesModel, 'bookSeries', 'createdAt', 'ASC'] ] }) for (const book of booksWithTag) { @@ -68,7 +72,7 @@ module.exports = { /** * Get all library items that have genres * @param {string[]} genres - * @returns {Promise} + * @returns {Promise} */ async getAllLibraryItemsWithGenres(genres) { const libraryItems = []