diff --git a/server/models/Book.js b/server/models/Book.js index 7e1a2e30c..d9f2ff132 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -647,11 +647,6 @@ class Book extends Model { } } - /** - * Minified book JSON for list/shelf endpoints. - * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded - * with the same value semantics. Only additive changes to expanded; never remove or rename keys. - */ toOldJSONMinified() { if (!this.authors) { throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`) @@ -674,12 +669,6 @@ class Book extends Model { } } - /** - * Expanded book JSON for item detail and socket events. - * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. - * - * @param {string} libraryItemId - */ toOldJSONExpanded(libraryItemId) { if (!libraryItemId) { throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`) @@ -692,12 +681,16 @@ class Book extends Model { } return { - ...this.toOldJSONMinified(), - libraryItemId, + id: this.id, + libraryItemId: libraryItemId, metadata: this.oldMetadataToJSONExpanded(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], audioFiles: structuredClone(this.audioFiles), chapters: structuredClone(this.chapters), ebookFile: structuredClone(this.ebookFile), + duration: this.duration, + size: this.size, tracks: this.getTracklist(libraryItemId) } } diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index c03667efc..b33fa585c 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1004,11 +1004,6 @@ class LibraryItem extends Model { } } - /** - * Minified library item JSON for list/shelf endpoints. - * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded - * with the same value semantics. Only additive changes to expanded; never remove or rename keys. - */ toOldJSONMinified() { if (!this.media) { throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`) @@ -1037,18 +1032,30 @@ class LibraryItem extends Model { } } - /** - * Expanded library item JSON for item detail and socket events. - * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. - */ toOldJSONExpanded() { return { - ...this.toOldJSONMinified(), + id: this.id, + ino: this.ino, + oldLibraryItemId: this.extraData?.oldLibraryItemId || null, + libraryId: this.libraryId, + folderId: this.libraryFolderId, + path: this.path, + relPath: this.relPath, + isFile: this.isFile, + mtimeMs: this.mtime?.valueOf(), + ctimeMs: this.ctime?.valueOf(), + birthtimeMs: this.birthtime?.valueOf(), + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf(), lastScan: this.lastScan?.valueOf(), scanVersion: this.lastScanVersion, + isMissing: !!this.isMissing, + isInvalid: !!this.isInvalid, + mediaType: this.mediaType, media: this.media.toOldJSONExpanded(this.id), // LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database - libraryFiles: this.getLibraryFilesJson() + libraryFiles: this.getLibraryFilesJson(), + size: this.size } } } diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 9c222e48a..6dbe1cd19 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -451,11 +451,6 @@ class Podcast extends Model { } } - /** - * Minified podcast JSON for list/shelf endpoints. - * `toOldJSONExpanded()` must be a strict superset: every key here must exist in expanded - * with the same value semantics. Only additive changes to expanded; never remove or rename keys. - */ toOldJSONMinified() { return { id: this.id, @@ -473,12 +468,6 @@ class Podcast extends Model { } } - /** - * Expanded podcast JSON for item detail and socket events. - * Must be a strict superset of `toOldJSONMinified()` — built by spreading minified, then adding expanded-only fields. - * - * @param {string} libraryItemId - */ toOldJSONExpanded(libraryItemId) { if (!libraryItemId) { throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`) @@ -488,9 +477,18 @@ class Podcast extends Model { } return { - ...this.toOldJSONMinified(), - libraryItemId, - episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)) + id: this.id, + libraryItemId: libraryItemId, + metadata: this.oldMetadataToJSONExpanded(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)), + autoDownloadEpisodes: this.autoDownloadEpisodes, + autoDownloadSchedule: this.autoDownloadSchedule, + lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null, + maxEpisodesToKeep: this.maxEpisodesToKeep, + maxNewEpisodesToDownload: this.maxNewEpisodesToDownload, + size: this.size } } } diff --git a/test/server/models/oldJsonSerialization.test.js b/test/server/models/oldJsonSerialization.test.js deleted file mode 100644 index 98bab6afa..000000000 --- a/test/server/models/oldJsonSerialization.test.js +++ /dev/null @@ -1,158 +0,0 @@ -const { expect } = require('chai') -const { Sequelize } = require('sequelize') - -const Database = require('../../../server/Database') - -/** - * Assert that `expanded` contains every key from `minified` with matching values. - * Used to enforce the contract: toOldJSONExpanded() is a strict superset of toOldJSONMinified(). - * - * @param {object} minified - * @param {object} expanded - * @param {string} [path] - */ -function assertExpandedSuperset(minified, expanded, path = '') { - for (const key of Object.keys(minified)) { - const keyPath = path ? `${path}.${key}` : key - expect(expanded, `missing key ${keyPath}`).to.have.property(key) - - const minifiedValue = minified[key] - const expandedValue = expanded[key] - - if (minifiedValue !== null && typeof minifiedValue === 'object' && !Array.isArray(minifiedValue)) { - assertExpandedSuperset(minifiedValue, expandedValue, keyPath) - } else if (Array.isArray(minifiedValue)) { - expect(expandedValue, `expected array at ${keyPath}`).to.be.an('array') - expect(expandedValue).to.deep.equal(minifiedValue) - } else { - expect(expandedValue, `value mismatch at ${keyPath}`).to.equal(minifiedValue) - } - } -} - -describe('old JSON serialization', () => { - let bookLibraryItemId - let podcastLibraryItemId - - beforeEach(async () => { - global.ServerSettings = {} - Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) - Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') - await Database.buildModels() - - const bookLibrary = await Database.libraryModel.create({ name: 'Book Library', mediaType: 'book' }) - const bookLibraryFolder = await Database.libraryFolderModel.create({ path: '/books', libraryId: bookLibrary.id }) - - const book = await Database.bookModel.create({ - title: 'Test Book', - audioFiles: [{ index: 1, ino: '1', metadata: { filename: 'track.mp3', ext: '.mp3', path: '/track.mp3', relPath: 'track.mp3', size: 1000 } }], - tags: ['fiction'], - narrators: ['Narrator One'], - genres: ['Fantasy'], - chapters: [{ id: 0, start: 0, end: 100, title: 'Chapter 1' }], - ebookFile: { ino: '2', metadata: { filename: 'book.epub', ext: '.epub', path: '/book.epub', relPath: 'book.epub', size: 500 }, ebookFormat: 'epub' } - }) - const bookLibraryItem = await Database.libraryItemModel.create({ - libraryFiles: [{ ino: '1', metadata: { filename: 'track.mp3', ext: '.mp3', path: '/track.mp3', relPath: 'track.mp3' } }], - mediaId: book.id, - mediaType: 'book', - libraryId: bookLibrary.id, - libraryFolderId: bookLibraryFolder.id - }) - bookLibraryItemId = bookLibraryItem.id - - const author = await Database.authorModel.create({ name: 'Test Author', libraryId: bookLibrary.id }) - await Database.bookAuthorModel.create({ bookId: book.id, authorId: author.id }) - - const series = await Database.seriesModel.create({ name: 'Test Series', libraryId: bookLibrary.id }) - await Database.bookSeriesModel.create({ bookId: book.id, seriesId: series.id, sequence: '2' }) - - const podcastLibrary = await Database.libraryModel.create({ name: 'Podcast Library', mediaType: 'podcast' }) - const podcastLibraryFolder = await Database.libraryFolderModel.create({ path: '/podcasts', libraryId: podcastLibrary.id }) - - const podcast = await Database.podcastModel.create({ - title: 'Test Podcast', - tags: ['news'], - genres: ['Technology'], - autoDownloadEpisodes: false - }) - const podcastLibraryItem = await Database.libraryItemModel.create({ - libraryFiles: [], - mediaId: podcast.id, - mediaType: 'podcast', - libraryId: podcastLibrary.id, - libraryFolderId: podcastLibraryFolder.id - }) - podcastLibraryItemId = podcastLibraryItem.id - - await Database.podcastEpisodeModel.create({ - podcastId: podcast.id, - title: 'Episode 1', - index: 1, - audioFile: { ino: '3', metadata: { filename: 'ep1.mp3', ext: '.mp3', path: '/ep1.mp3', relPath: 'ep1.mp3' } } - }) - }) - - afterEach(async () => { - await Database.sequelize.sync({ force: true }) - }) - - describe('Book', () => { - it('toOldJSONExpanded is a strict superset of toOldJSONMinified', async () => { - const libraryItem = await Database.libraryItemModel.getExpandedById(bookLibraryItemId) - const minified = libraryItem.media.toOldJSONMinified() - const expanded = libraryItem.media.toOldJSONExpanded(libraryItem.id) - - assertExpandedSuperset(minified, expanded) - - expect(expanded).to.have.property('libraryItemId', libraryItem.id) - expect(expanded).to.have.property('audioFiles') - expect(expanded).to.have.property('chapters') - expect(expanded).to.have.property('tracks') - expect(expanded.numTracks).to.equal(expanded.tracks.length) - expect(expanded.numAudioFiles).to.equal(expanded.audioFiles.length) - expect(expanded.numChapters).to.equal(expanded.chapters.length) - expect(expanded.ebookFormat).to.equal('epub') - }) - }) - - describe('Podcast', () => { - it('toOldJSONExpanded is a strict superset of toOldJSONMinified', async () => { - const libraryItem = await Database.libraryItemModel.getExpandedById(podcastLibraryItemId) - const minified = libraryItem.media.toOldJSONMinified() - const expanded = libraryItem.media.toOldJSONExpanded(libraryItem.id) - - assertExpandedSuperset(minified, expanded) - - expect(expanded).to.have.property('libraryItemId', libraryItem.id) - expect(expanded).to.have.property('episodes') - expect(expanded.numEpisodes).to.equal(expanded.episodes.length) - }) - }) - - describe('LibraryItem', () => { - it('book library item expanded is a strict superset of minified', async () => { - const libraryItem = await Database.libraryItemModel.getExpandedById(bookLibraryItemId) - const minified = libraryItem.toOldJSONMinified() - const expanded = libraryItem.toOldJSONExpanded() - - assertExpandedSuperset(minified, expanded) - - expect(expanded).to.have.property('libraryFiles') - expect(expanded).to.have.property('lastScan') - expect(expanded.numFiles).to.equal(expanded.libraryFiles.length) - expect(expanded.media.numTracks).to.equal(expanded.media.tracks.length) - }) - - it('podcast library item expanded is a strict superset of minified', async () => { - const libraryItem = await Database.libraryItemModel.getExpandedById(podcastLibraryItemId) - const minified = libraryItem.toOldJSONMinified() - const expanded = libraryItem.toOldJSONExpanded() - - assertExpandedSuperset(minified, expanded) - - expect(expanded).to.have.property('libraryFiles') - expect(expanded.media.numEpisodes).to.equal(expanded.media.episodes.length) - }) - }) -})