mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
Merge 3bb53d939d into 6f03467f35
This commit is contained in:
commit
6432c0059b
4 changed files with 196 additions and 36 deletions
|
|
@ -647,6 +647,11 @@ 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() {
|
toOldJSONMinified() {
|
||||||
if (!this.authors) {
|
if (!this.authors) {
|
||||||
throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
|
throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`)
|
||||||
|
|
@ -669,6 +674,12 @@ 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) {
|
toOldJSONExpanded(libraryItemId) {
|
||||||
if (!libraryItemId) {
|
if (!libraryItemId) {
|
||||||
throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
|
throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
|
@ -681,16 +692,12 @@ class Book extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
...this.toOldJSONMinified(),
|
||||||
libraryItemId: libraryItemId,
|
libraryItemId,
|
||||||
metadata: this.oldMetadataToJSONExpanded(),
|
metadata: this.oldMetadataToJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
|
||||||
tags: [...(this.tags || [])],
|
|
||||||
audioFiles: structuredClone(this.audioFiles),
|
audioFiles: structuredClone(this.audioFiles),
|
||||||
chapters: structuredClone(this.chapters),
|
chapters: structuredClone(this.chapters),
|
||||||
ebookFile: structuredClone(this.ebookFile),
|
ebookFile: structuredClone(this.ebookFile),
|
||||||
duration: this.duration,
|
|
||||||
size: this.size,
|
|
||||||
tracks: this.getTracklist(libraryItemId)
|
tracks: this.getTracklist(libraryItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1004,6 +1004,11 @@ 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() {
|
toOldJSONMinified() {
|
||||||
if (!this.media) {
|
if (!this.media) {
|
||||||
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
||||||
|
|
@ -1032,30 +1037,18 @@ 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() {
|
toOldJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
...this.toOldJSONMinified(),
|
||||||
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(),
|
lastScan: this.lastScan?.valueOf(),
|
||||||
scanVersion: this.lastScanVersion,
|
scanVersion: this.lastScanVersion,
|
||||||
isMissing: !!this.isMissing,
|
|
||||||
isInvalid: !!this.isInvalid,
|
|
||||||
mediaType: this.mediaType,
|
|
||||||
media: this.media.toOldJSONExpanded(this.id),
|
media: this.media.toOldJSONExpanded(this.id),
|
||||||
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,11 @@ 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() {
|
toOldJSONMinified() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
@ -468,6 +473,12 @@ 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) {
|
toOldJSONExpanded(libraryItemId) {
|
||||||
if (!libraryItemId) {
|
if (!libraryItemId) {
|
||||||
throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
|
throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`)
|
||||||
|
|
@ -477,18 +488,9 @@ class Podcast extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
...this.toOldJSONMinified(),
|
||||||
libraryItemId: libraryItemId,
|
libraryItemId,
|
||||||
metadata: this.oldMetadataToJSONExpanded(),
|
episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
158
test/server/models/oldJsonSerialization.test.js
Normal file
158
test/server/models/oldJsonSerialization.test.js
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue