mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-30 17:59:21 +01:00
Update bookSeries & bookAuthors table to include createdAt timestamp
This commit is contained in:
parent
4dbe8d29d9
commit
4d0acb30ba
@ -21,7 +21,8 @@ module.exports = (sequelize) => {
|
|||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'bookAuthor',
|
modelName: 'bookAuthor',
|
||||||
timestamps: false
|
timestamps: true,
|
||||||
|
updatedAt: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Super Many-to-Many
|
// Super Many-to-Many
|
||||||
|
@ -22,7 +22,8 @@ module.exports = (sequelize) => {
|
|||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'bookSeries',
|
modelName: 'bookSeries',
|
||||||
timestamps: false
|
timestamps: true,
|
||||||
|
updatedAt: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Super Many-to-Many
|
// Super Many-to-Many
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model, literal } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const oldLibraryItem = require('../objects/LibraryItem')
|
const oldLibraryItem = require('../objects/LibraryItem')
|
||||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||||
@ -70,13 +70,13 @@ module.exports = (sequelize) => {
|
|||||||
{
|
{
|
||||||
model: sequelize.models.author,
|
model: sequelize.models.author,
|
||||||
through: {
|
through: {
|
||||||
attributes: []
|
attributes: ['createdAt']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: sequelize.models.series,
|
model: sequelize.models.series,
|
||||||
through: {
|
through: {
|
||||||
attributes: ['sequence']
|
attributes: ['sequence', 'createdAt']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -85,6 +85,12 @@ module.exports = (sequelize) => {
|
|||||||
model: sequelize.models.podcast
|
model: sequelize.models.podcast
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
order: [
|
||||||
|
['createdAt', 'ASC'],
|
||||||
|
// Ensure author & series stay in the same order
|
||||||
|
[sequelize.models.book, sequelize.models.author, sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
||||||
|
[sequelize.models.book, sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'],
|
||||||
|
],
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit
|
||||||
})
|
})
|
||||||
|
@ -73,7 +73,6 @@ module.exports = {
|
|||||||
} else if (filterBy === 'feed-open') {
|
} else if (filterBy === 'feed-open') {
|
||||||
const libraryItemIdsWithFeed = await Database.models.feed.findAllLibraryItemIds()
|
const libraryItemIdsWithFeed = await Database.models.feed.findAllLibraryItemIds()
|
||||||
filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id))
|
filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id))
|
||||||
// filtered = filtered.filter(li => feedsArray.some(feed => feed.entityId === li.id))
|
|
||||||
} else if (filterBy === 'abridged') {
|
} else if (filterBy === 'abridged') {
|
||||||
filtered = filtered.filter(li => !!li.media.metadata?.abridged)
|
filtered = filtered.filter(li => !!li.media.metadata?.abridged)
|
||||||
} else if (filterBy === 'ebook') {
|
} else if (filterBy === 'ebook') {
|
||||||
|
@ -1543,6 +1543,76 @@ async function migrationPatch2Authors(ctx, offset = 0) {
|
|||||||
return migrationPatch2Authors(ctx, offset + authors.length)
|
return migrationPatch2Authors(ctx, offset + authors.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.3 to 2.3.4
|
||||||
|
* Populating the createdAt column on bookAuthor
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
async function migrationPatch2BookAuthors(ctx, offset = 0) {
|
||||||
|
const bookAuthors = await ctx.models.bookAuthor.findAll({
|
||||||
|
include: {
|
||||||
|
model: ctx.models.author
|
||||||
|
},
|
||||||
|
limit: 500,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
if (!bookAuthors.length) return
|
||||||
|
|
||||||
|
const bulkUpdateItems = []
|
||||||
|
for (const bookAuthor of bookAuthors) {
|
||||||
|
if (bookAuthor.author?.createdAt) {
|
||||||
|
const dateString = bookAuthor.author.createdAt.toISOString().replace('T', ' ').replace('Z', '')
|
||||||
|
bulkUpdateItems.push(`("${bookAuthor.id}","${dateString}")`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkUpdateItems.length) {
|
||||||
|
Logger.info(`[dbMigration] Migration patch 2.3.3+ - patching ${bulkUpdateItems.length} bookAuthors`)
|
||||||
|
await ctx.sequelize.query(`INSERT INTO bookAuthors ('id','createdAt') VALUES ${bulkUpdateItems.join(',')} ON CONFLICT(id) DO UPDATE SET 'createdAt' = EXCLUDED.createdAt;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookAuthors.length < 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return migrationPatch2BookAuthors(ctx, offset + bookAuthors.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.3 to 2.3.4
|
||||||
|
* Populating the createdAt column on bookSeries
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
async function migrationPatch2BookSeries(ctx, offset = 0) {
|
||||||
|
const allBookSeries = await ctx.models.bookSeries.findAll({
|
||||||
|
include: {
|
||||||
|
model: ctx.models.series
|
||||||
|
},
|
||||||
|
limit: 500,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
if (!allBookSeries.length) return
|
||||||
|
|
||||||
|
const bulkUpdateItems = []
|
||||||
|
for (const bookSeries of allBookSeries) {
|
||||||
|
if (bookSeries.series?.createdAt) {
|
||||||
|
const dateString = bookSeries.series.createdAt.toISOString().replace('T', ' ').replace('Z', '')
|
||||||
|
bulkUpdateItems.push(`("${bookSeries.id}","${dateString}")`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkUpdateItems.length) {
|
||||||
|
Logger.info(`[dbMigration] Migration patch 2.3.3+ - patching ${bulkUpdateItems.length} bookSeries`)
|
||||||
|
await ctx.sequelize.query(`INSERT INTO bookSeries ('id','createdAt') VALUES ${bulkUpdateItems.join(',')} ON CONFLICT(id) DO UPDATE SET 'createdAt' = EXCLUDED.createdAt;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allBookSeries.length < 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return migrationPatch2BookSeries(ctx, offset + allBookSeries.length)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration from 2.3.3 to 2.3.4
|
* Migration from 2.3.3 to 2.3.4
|
||||||
* Adding coverPath column to Feed model
|
* Adding coverPath column to Feed model
|
||||||
@ -1552,8 +1622,9 @@ module.exports.migrationPatch2 = async (ctx) => {
|
|||||||
const queryInterface = ctx.sequelize.getQueryInterface()
|
const queryInterface = ctx.sequelize.getQueryInterface()
|
||||||
const feedTableDescription = await queryInterface.describeTable('feeds')
|
const feedTableDescription = await queryInterface.describeTable('feeds')
|
||||||
const authorsTableDescription = await queryInterface.describeTable('authors')
|
const authorsTableDescription = await queryInterface.describeTable('authors')
|
||||||
|
const bookAuthorsTableDescription = await queryInterface.describeTable('bookAuthors')
|
||||||
|
|
||||||
if (feedTableDescription?.coverPath && authorsTableDescription?.lastFirst) {
|
if (feedTableDescription?.coverPath && authorsTableDescription?.lastFirst && bookAuthorsTableDescription?.createdAt) {
|
||||||
Logger.info(`[dbMigration] Migration patch 2.3.3+ - columns already on model`)
|
Logger.info(`[dbMigration] Migration patch 2.3.3+ - columns already on model`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1562,25 +1633,35 @@ module.exports.migrationPatch2 = async (ctx) => {
|
|||||||
try {
|
try {
|
||||||
await queryInterface.sequelize.transaction(t => {
|
await queryInterface.sequelize.transaction(t => {
|
||||||
const queries = [
|
const queries = [
|
||||||
queryInterface.addColumn('authors', 'lastFirst', {
|
queryInterface.addColumn('bookAuthors', 'createdAt', {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.DATE
|
||||||
}, { transaction: t }),
|
}, { transaction: t }),
|
||||||
queryInterface.addColumn('libraryItems', 'size', {
|
queryInterface.addColumn('bookSeries', 'createdAt', {
|
||||||
type: DataTypes.BIGINT
|
type: DataTypes.DATE
|
||||||
}, { transaction: t }),
|
|
||||||
queryInterface.addColumn('books', 'duration', {
|
|
||||||
type: DataTypes.FLOAT
|
|
||||||
}, { transaction: t }),
|
|
||||||
queryInterface.addColumn('books', 'titleIgnorePrefix', {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
}, { transaction: t }),
|
|
||||||
queryInterface.addColumn('podcasts', 'titleIgnorePrefix', {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
}, { transaction: t }),
|
|
||||||
queryInterface.addColumn('series', 'nameIgnorePrefix', {
|
|
||||||
type: DataTypes.STRING
|
|
||||||
}, { transaction: t }),
|
}, { transaction: t }),
|
||||||
]
|
]
|
||||||
|
if (!authorsTableDescription?.lastFirst) {
|
||||||
|
queries.push(...[
|
||||||
|
queryInterface.addColumn('authors', 'lastFirst', {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('libraryItems', 'size', {
|
||||||
|
type: DataTypes.BIGINT
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('books', 'duration', {
|
||||||
|
type: DataTypes.FLOAT
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('books', 'titleIgnorePrefix', {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('podcasts', 'titleIgnorePrefix', {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('series', 'nameIgnorePrefix', {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
}, { transaction: t }),
|
||||||
|
])
|
||||||
|
}
|
||||||
if (!feedTableDescription?.coverPath) {
|
if (!feedTableDescription?.coverPath) {
|
||||||
queries.push(queryInterface.addColumn('feeds', 'coverPath', {
|
queries.push(queryInterface.addColumn('feeds', 'coverPath', {
|
||||||
type: DataTypes.STRING
|
type: DataTypes.STRING
|
||||||
@ -1589,24 +1670,32 @@ module.exports.migrationPatch2 = async (ctx) => {
|
|||||||
return Promise.all(queries)
|
return Promise.all(queries)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (global.ServerSettings.sortingPrefixes?.length) {
|
if (!authorsTableDescription?.lastFirst) {
|
||||||
prefixesToIgnore = global.ServerSettings.sortingPrefixes
|
if (global.ServerSettings.sortingPrefixes?.length) {
|
||||||
|
prefixesToIgnore = global.ServerSettings.sortingPrefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch library items size column
|
||||||
|
await migrationPatch2LibraryItems(ctx, 0)
|
||||||
|
|
||||||
|
// Patch books duration & titleIgnorePrefix column
|
||||||
|
await migrationPatch2Books(ctx, 0)
|
||||||
|
|
||||||
|
// Patch podcasts titleIgnorePrefix column
|
||||||
|
await migrationPatch2Podcasts(ctx, 0)
|
||||||
|
|
||||||
|
// Patch authors lastFirst column
|
||||||
|
await migrationPatch2Authors(ctx, 0)
|
||||||
|
|
||||||
|
// Patch series nameIgnorePrefix column
|
||||||
|
await migrationPatch2Series(ctx, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch library items size column
|
// Patch bookAuthors createdAt column
|
||||||
await migrationPatch2LibraryItems(ctx, 0)
|
await migrationPatch2BookAuthors(ctx, 0)
|
||||||
|
|
||||||
// Patch books duration & titleIgnorePrefix column
|
// Patch bookSeries createdAt column
|
||||||
await migrationPatch2Books(ctx, 0)
|
await migrationPatch2BookSeries(ctx, 0)
|
||||||
|
|
||||||
// Patch podcasts titleIgnorePrefix column
|
|
||||||
await migrationPatch2Podcasts(ctx, 0)
|
|
||||||
|
|
||||||
// Patch authors lastFirst column
|
|
||||||
await migrationPatch2Authors(ctx, 0)
|
|
||||||
|
|
||||||
// Patch series nameIgnorePrefix column
|
|
||||||
await migrationPatch2Series(ctx, 0)
|
|
||||||
|
|
||||||
Logger.info(`[dbMigration] Migration patch 2.3.3+ finished`)
|
Logger.info(`[dbMigration] Migration patch 2.3.3+ finished`)
|
||||||
return true
|
return true
|
||||||
|
@ -1,154 +1,21 @@
|
|||||||
const { Op, literal, col, fn, where } = require('sequelize')
|
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||||
const Database = require('../../Database')
|
|
||||||
const libraryItemsSeriesFilters = require('./libraryItemsSeriesFilters')
|
|
||||||
const libraryItemsProgressFilters = require('./libraryItemsProgressFilters')
|
|
||||||
const Logger = require('../../Logger')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
decode(text) {
|
decode(text) {
|
||||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
},
|
},
|
||||||
|
|
||||||
getMediaGroupQuery(group, value) {
|
|
||||||
let mediaWhere = {}
|
|
||||||
|
|
||||||
if (['genres', 'tags', 'narrators'].includes(group)) {
|
|
||||||
mediaWhere[group] = {
|
|
||||||
[Op.substring]: `"${value}"`
|
|
||||||
}
|
|
||||||
} else if (group === 'publishers') {
|
|
||||||
mediaWhere['publisher'] = {
|
|
||||||
[Op.substring]: `"${value}"`
|
|
||||||
}
|
|
||||||
} else if (group === 'languages') {
|
|
||||||
mediaWhere['language'] = {
|
|
||||||
[Op.substring]: `"${value}"`
|
|
||||||
}
|
|
||||||
} else if (group === 'tracks') {
|
|
||||||
if (value === 'multi') {
|
|
||||||
mediaWhere = where(fn('json_array_length', col('audioFiles')), {
|
|
||||||
[Op.gt]: 1
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
mediaWhere = where(fn('json_array_length', col('audioFiles')), 1)
|
|
||||||
}
|
|
||||||
} else if (group === 'ebooks') {
|
|
||||||
if (value === 'ebook') {
|
|
||||||
mediaWhere['ebookFile'] = {
|
|
||||||
[Op.not]: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaWhere
|
|
||||||
},
|
|
||||||
|
|
||||||
getOrder(sortBy, sortDesc) {
|
|
||||||
const dir = sortDesc ? 'DESC' : 'ASC'
|
|
||||||
if (sortBy === 'addedAt') {
|
|
||||||
return [['createdAt', dir]]
|
|
||||||
} else if (sortBy === 'size') {
|
|
||||||
return [['size', dir]]
|
|
||||||
} else if (sortBy === 'birthtimeMs') {
|
|
||||||
return [['birthtime', dir]]
|
|
||||||
} else if (sortBy === 'mtimeMs') {
|
|
||||||
return [['mtime', dir]]
|
|
||||||
} else if (sortBy === 'media.duration') {
|
|
||||||
return [[literal('book.duration'), dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.publishedYear') {
|
|
||||||
return [[literal('book.publishedYear'), dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.authorNameLF') {
|
|
||||||
return [[literal('book.authors.lastFirst'), dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.authorName') {
|
|
||||||
return [[literal('book.authors.name'), dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.title') {
|
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
|
||||||
return [[literal('book.titleIgnorePrefix'), dir]]
|
|
||||||
} else {
|
|
||||||
return [[literal('book.title'), dir]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
|
|
||||||
async getFilteredLibraryItems(libraryId, filterBy, sortBy, sortDesc, limit, offset, userId) {
|
async getFilteredLibraryItems(libraryId, filterBy, sortBy, sortDesc, limit, offset, userId) {
|
||||||
const libraryItemModel = Database.models.libraryItem
|
let filterValue = null
|
||||||
|
let filterGroup = null
|
||||||
let mediaWhereQuery = null
|
if (filterBy) {
|
||||||
let mediaAttributes = null
|
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
|
||||||
let itemWhereQuery = {
|
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
libraryId
|
filterGroup = group || filterBy
|
||||||
|
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemIncludes = []
|
// TODO: Handle podcast filters
|
||||||
|
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset)
|
||||||
let authorInclude = {
|
|
||||||
model: Database.models.author,
|
|
||||||
through: {
|
|
||||||
attributes: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let seriesInclude = {
|
|
||||||
model: Database.models.series,
|
|
||||||
through: {
|
|
||||||
attributes: ['sequence']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
|
|
||||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
|
||||||
if (group) {
|
|
||||||
// e.g. genre id
|
|
||||||
const value = this.decode(filterBy.replace(`${group}.`, ''))
|
|
||||||
|
|
||||||
if (group === 'series' && value === 'no-series') {
|
|
||||||
return libraryItemsSeriesFilters.getLibraryItemsWithNoSeries(libraryId, sortBy, sortDesc, limit, offset)
|
|
||||||
} else if (group === 'progress') {
|
|
||||||
return libraryItemsProgressFilters.getLibraryItemsWithProgressFilter(value, libraryId, userId, sortBy, sortDesc, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group === 'authors') {
|
|
||||||
authorInclude.where = {
|
|
||||||
id: value
|
|
||||||
}
|
|
||||||
authorInclude.required = true
|
|
||||||
} else if (group === 'series') {
|
|
||||||
seriesInclude.where = {
|
|
||||||
id: value
|
|
||||||
}
|
|
||||||
seriesInclude.required = true
|
|
||||||
} else {
|
|
||||||
mediaWhereQuery = this.getMediaGroupQuery(group, value)
|
|
||||||
}
|
|
||||||
} else if (filterBy === 'abridged') {
|
|
||||||
mediaWhereQuery = {
|
|
||||||
abridged: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { rows: libraryItems, count } = await libraryItemModel.findAndCountAll({
|
|
||||||
where: itemWhereQuery,
|
|
||||||
attributes: {
|
|
||||||
include: [
|
|
||||||
[fn('group_concat', col('book.author.name'), ', '), 'author_name']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
distinct: true,
|
|
||||||
subQuery: false,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.models.book,
|
|
||||||
attributes: mediaAttributes,
|
|
||||||
where: mediaWhereQuery,
|
|
||||||
required: true,
|
|
||||||
include: [authorInclude, seriesInclude, ...itemIncludes]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: this.getOrder(sortBy, sortDesc),
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
})
|
|
||||||
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
|
|
||||||
return { libraryItems, count }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
303
server/utils/queries/libraryItemsBookFilters.js
Normal file
303
server/utils/queries/libraryItemsBookFilters.js
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
const Sequelize = require('sequelize')
|
||||||
|
const Database = require('../../Database')
|
||||||
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Get where options for Book model
|
||||||
|
* @param {string} group
|
||||||
|
* @param {[string]} value
|
||||||
|
* @returns {Sequelize.WhereOptions}
|
||||||
|
*/
|
||||||
|
getMediaGroupQuery(group, value) {
|
||||||
|
let mediaWhere = {}
|
||||||
|
|
||||||
|
if (group === 'progress') {
|
||||||
|
if (value === 'not-finished') {
|
||||||
|
mediaWhere['$mediaProgresses.isFinished$'] = {
|
||||||
|
[Sequelize.Op.or]: [null, false]
|
||||||
|
}
|
||||||
|
} else if (value === 'not-started') {
|
||||||
|
mediaWhere[Sequelize.Op.and] = [
|
||||||
|
{
|
||||||
|
'$mediaProgresses.currentTime$': {
|
||||||
|
[Sequelize.Op.or]: [null, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$mediaProgresses.isFinished$': {
|
||||||
|
[Sequelize.Op.or]: [null, false]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else if (value === 'finished') {
|
||||||
|
mediaWhere['$mediaProgresses.isFinished$'] = true
|
||||||
|
} else if (value === 'in-progress') {
|
||||||
|
mediaWhere[Sequelize.Op.and] = [
|
||||||
|
{
|
||||||
|
[Sequelize.Op.or]: [
|
||||||
|
{
|
||||||
|
'$mediaProgresses.currentTime$': {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$mediaProgresses.ebookProgress$': {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$mediaProgresses.isFinished$': false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (group === 'series' && value === 'no-series') {
|
||||||
|
mediaWhere['$series.id$'] = null
|
||||||
|
} else if (group === 'abridged') {
|
||||||
|
mediaWhere['abridged'] = true
|
||||||
|
} else if (['genres', 'tags', 'narrators'].includes(group)) {
|
||||||
|
mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = "${value}")`), {
|
||||||
|
[Sequelize.Op.gte]: 1
|
||||||
|
})
|
||||||
|
} else if (group === 'publishers') {
|
||||||
|
mediaWhere['publisher'] = value
|
||||||
|
} else if (group === 'languages') {
|
||||||
|
mediaWhere['language'] = value
|
||||||
|
} else if (group === 'tracks') {
|
||||||
|
if (value === 'multi') {
|
||||||
|
mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), {
|
||||||
|
[Sequelize.Op.gt]: 1
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 1)
|
||||||
|
}
|
||||||
|
} else if (group === 'ebooks') {
|
||||||
|
if (value === 'ebook') {
|
||||||
|
mediaWhere['ebookFile'] = {
|
||||||
|
[Sequelize.Op.not]: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (group === 'missing') {
|
||||||
|
if (['asin', 'isbn', 'subtitle', 'publishedYear', 'description', 'publisher', 'language', 'cover'].includes(value)) {
|
||||||
|
let key = value
|
||||||
|
if (value === 'cover') key = 'coverPath'
|
||||||
|
mediaWhere[key] = {
|
||||||
|
[Sequelize.Op.or]: [null, '']
|
||||||
|
}
|
||||||
|
} else if (['genres', 'tags', 'narrator'].includes(value)) {
|
||||||
|
mediaWhere[value] = {
|
||||||
|
[Sequelize.Op.or]: [null, Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col(value)), 0)]
|
||||||
|
}
|
||||||
|
} else if (value === 'authors') {
|
||||||
|
mediaWhere['$authors.id$'] = null
|
||||||
|
} else if (value === 'series') {
|
||||||
|
mediaWhere['$series.id$'] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaWhere
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sequelize order
|
||||||
|
* @param {string} sortBy
|
||||||
|
* @param {boolean} sortDesc
|
||||||
|
* @returns {Sequelize.order}
|
||||||
|
*/
|
||||||
|
getOrder(sortBy, sortDesc) {
|
||||||
|
const dir = sortDesc ? 'DESC' : 'ASC'
|
||||||
|
if (sortBy === 'addedAt') {
|
||||||
|
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
|
||||||
|
} else if (sortBy === 'size') {
|
||||||
|
return [[Sequelize.literal('libraryItem.size'), dir]]
|
||||||
|
} else if (sortBy === 'birthtimeMs') {
|
||||||
|
return [[Sequelize.literal('libraryItem.birthtime'), dir]]
|
||||||
|
} else if (sortBy === 'mtimeMs') {
|
||||||
|
return [[Sequelize.literal('libraryItem.mtime'), dir]]
|
||||||
|
} else if (sortBy === 'media.duration') {
|
||||||
|
return [['duration', dir]]
|
||||||
|
} else if (sortBy === 'media.metadata.publishedYear') {
|
||||||
|
return [['publishedYear', dir]]
|
||||||
|
} else if (sortBy === 'media.metadata.authorNameLF') {
|
||||||
|
return [['author_name', dir]]
|
||||||
|
} else if (sortBy === 'media.metadata.authorName') {
|
||||||
|
return [['author_name', dir]]
|
||||||
|
} else if (sortBy === 'media.metadata.title') {
|
||||||
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
|
return [['titleIgnorePrefix', dir]]
|
||||||
|
} else {
|
||||||
|
return [['title', dir]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get library items for book media type using filter and sort
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {[string]} filterGroup
|
||||||
|
* @param {[string]} filterValue
|
||||||
|
* @param {string} sortBy
|
||||||
|
* @param {string} sortDesc
|
||||||
|
* @param {number} limit
|
||||||
|
* @param {number} offset
|
||||||
|
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||||
|
*/
|
||||||
|
async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) {
|
||||||
|
// For sorting by author name an additional attribute must be added
|
||||||
|
// with author names concatenated
|
||||||
|
let bookAttributes = null
|
||||||
|
if (sortBy === 'media.metadata.authorNameLF') {
|
||||||
|
bookAttributes = {
|
||||||
|
include: [
|
||||||
|
[Sequelize.literal(`(SELECT group_concat(a.lastFirst, ", ") FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id)`), 'author_name']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else if (sortBy === 'media.metadata.authorName') {
|
||||||
|
bookAttributes = {
|
||||||
|
include: [
|
||||||
|
[Sequelize.literal(`(SELECT group_concat(a.name, ", ") FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id)`), 'author_name']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItemWhere = {
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
|
||||||
|
let seriesInclude = {
|
||||||
|
model: Database.models.bookSeries,
|
||||||
|
attributes: ['seriesId', 'sequence', 'createdAt'],
|
||||||
|
include: {
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
},
|
||||||
|
order: [
|
||||||
|
['createdAt', 'ASC']
|
||||||
|
],
|
||||||
|
separate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
let authorInclude = {
|
||||||
|
model: Database.models.bookAuthor,
|
||||||
|
attributes: ['authorId', 'createdAt'],
|
||||||
|
include: {
|
||||||
|
model: Database.models.author,
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
},
|
||||||
|
order: [
|
||||||
|
['createdAt', 'ASC']
|
||||||
|
],
|
||||||
|
separate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItemIncludes = []
|
||||||
|
const bookIncludes = []
|
||||||
|
if (filterGroup === 'feed-open') {
|
||||||
|
libraryItemIncludes.push({
|
||||||
|
model: Database.models.feed,
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
} else if (filterGroup === 'ebooks' && filterValue === 'supplementary') {
|
||||||
|
// TODO: Temp workaround for filtering supplementary ebook
|
||||||
|
libraryItemWhere['libraryFiles'] = {
|
||||||
|
[Sequelize.Op.substring]: `"isSupplementary":true`
|
||||||
|
}
|
||||||
|
} else if (filterGroup === 'missing' && filterValue === 'authors') {
|
||||||
|
authorInclude = {
|
||||||
|
model: Database.models.author,
|
||||||
|
attributes: ['id'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((filterGroup === 'series' && filterValue === 'no-series') || (filterGroup === 'missing' && filterValue === 'series')) {
|
||||||
|
seriesInclude = {
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (filterGroup === 'authors') {
|
||||||
|
bookIncludes.push({
|
||||||
|
model: Database.models.author,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
where: {
|
||||||
|
id: filterValue
|
||||||
|
},
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (filterGroup === 'series') {
|
||||||
|
bookIncludes.push({
|
||||||
|
model: Database.models.series,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
where: {
|
||||||
|
id: filterValue
|
||||||
|
},
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (filterGroup === 'issues') {
|
||||||
|
libraryItemWhere[Sequelize.Op.or] = [
|
||||||
|
{
|
||||||
|
isMissing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isInvalid: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else if (filterGroup === 'progress') {
|
||||||
|
bookIncludes.push({
|
||||||
|
model: Database.models.mediaProgress,
|
||||||
|
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress'],
|
||||||
|
where: {
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
||||||
|
where: filterGroup ? this.getMediaGroupQuery(filterGroup, filterValue) : null,
|
||||||
|
distinct: true,
|
||||||
|
attributes: bookAttributes,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.libraryItem,
|
||||||
|
required: true,
|
||||||
|
where: libraryItemWhere,
|
||||||
|
include: libraryItemIncludes
|
||||||
|
},
|
||||||
|
seriesInclude,
|
||||||
|
authorInclude,
|
||||||
|
...bookIncludes
|
||||||
|
],
|
||||||
|
order: this.getOrder(sortBy, sortDesc),
|
||||||
|
subQuery: false,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
const libraryItems = books.map((bookExpanded) => {
|
||||||
|
const libraryItem = bookExpanded.libraryItem.toJSON()
|
||||||
|
const book = bookExpanded.toJSON()
|
||||||
|
delete book.libraryItem
|
||||||
|
delete book.authors
|
||||||
|
delete book.series
|
||||||
|
libraryItem.media = book
|
||||||
|
|
||||||
|
return libraryItem
|
||||||
|
})
|
||||||
|
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
|
||||||
|
return {
|
||||||
|
libraryItems,
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,130 +0,0 @@
|
|||||||
const Sequelize = require('sequelize')
|
|
||||||
const Database = require('../../Database')
|
|
||||||
const Logger = require('../../Logger')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getOrder(sortBy, sortDesc) {
|
|
||||||
const dir = sortDesc ? 'DESC' : 'ASC'
|
|
||||||
if (sortBy === 'addedAt') {
|
|
||||||
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
|
|
||||||
} else if (sortBy === 'size') {
|
|
||||||
return [[Sequelize.literal('libraryItem.size'), dir]]
|
|
||||||
} else if (sortBy === 'birthtimeMs') {
|
|
||||||
return [[Sequelize.literal('libraryItem.birthtime'), dir]]
|
|
||||||
} else if (sortBy === 'mtimeMs') {
|
|
||||||
return [[Sequelize.literal('libraryItem.mtime'), dir]]
|
|
||||||
} else if (sortBy === 'media.duration') {
|
|
||||||
return [['duration', dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.publishedYear') {
|
|
||||||
return [['publishedYear', dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.authorNameLF') {
|
|
||||||
return [] // TODO: Handle author filter
|
|
||||||
} else if (sortBy === 'media.metadata.authorName') {
|
|
||||||
return [] // TODO: Handle author filter
|
|
||||||
} else if (sortBy === 'media.metadata.title') {
|
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
|
||||||
return [['titleIgnorePrefix', dir]]
|
|
||||||
} else {
|
|
||||||
return [['title', dir]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
|
|
||||||
async getLibraryItemsWithProgressFilter(filterValue, libraryId, userId, sortBy, sortDesc, limit, offset) {
|
|
||||||
|
|
||||||
const bookWhere = {}
|
|
||||||
if (filterValue === 'not-finished') {
|
|
||||||
bookWhere['$mediaProgresses.isFinished$'] = {
|
|
||||||
[Sequelize.Op.or]: [null, false]
|
|
||||||
}
|
|
||||||
} else if (filterValue === 'not-started') {
|
|
||||||
bookWhere['$mediaProgresses.currentTime$'] = {
|
|
||||||
[Sequelize.Op.or]: [null, 0]
|
|
||||||
}
|
|
||||||
} else if (filterValue === 'finished') {
|
|
||||||
bookWhere['$mediaProgresses.isFinished$'] = true
|
|
||||||
} else { // in-progress
|
|
||||||
bookWhere[Sequelize.Op.and] = [
|
|
||||||
{
|
|
||||||
'$book.mediaProgresses.currentTime$': {
|
|
||||||
[Sequelize.Op.gt]: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'$book.mediaProgresses.isFinished$': false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
|
||||||
where: bookWhere,
|
|
||||||
distinct: true,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.models.libraryItem,
|
|
||||||
required: true,
|
|
||||||
where: {
|
|
||||||
libraryId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.models.bookSeries,
|
|
||||||
attributes: ['seriesId', 'sequence'],
|
|
||||||
include: {
|
|
||||||
model: Database.models.series,
|
|
||||||
attributes: ['id', 'name']
|
|
||||||
},
|
|
||||||
separate: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.models.bookAuthor,
|
|
||||||
attributes: ['authorId'],
|
|
||||||
include: {
|
|
||||||
model: Database.models.author,
|
|
||||||
attributes: ['id', 'name']
|
|
||||||
},
|
|
||||||
separate: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.models.mediaProgress,
|
|
||||||
attributes: ['id', 'isFinished'],
|
|
||||||
where: {
|
|
||||||
userId
|
|
||||||
},
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: this.getOrder(sortBy, sortDesc),
|
|
||||||
subQuery: false,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
})
|
|
||||||
|
|
||||||
const libraryItems = books.map((bookExpanded) => {
|
|
||||||
const libraryItem = bookExpanded.libraryItem.toJSON()
|
|
||||||
const book = bookExpanded.toJSON()
|
|
||||||
delete book.libraryItem
|
|
||||||
|
|
||||||
book.authors = []
|
|
||||||
if (book.bookAuthors?.length) {
|
|
||||||
book.bookAuthors.forEach((ba) => {
|
|
||||||
if (ba.author) {
|
|
||||||
book.authors.push(ba.author)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
delete book.bookAuthors
|
|
||||||
|
|
||||||
libraryItem.media = book
|
|
||||||
|
|
||||||
return libraryItem
|
|
||||||
})
|
|
||||||
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
|
|
||||||
return {
|
|
||||||
libraryItems,
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
const Sequelize = require('sequelize')
|
|
||||||
const Database = require('../../Database')
|
|
||||||
const Logger = require('../../Logger')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getOrder(sortBy, sortDesc) {
|
|
||||||
const dir = sortDesc ? 'DESC' : 'ASC'
|
|
||||||
if (sortBy === 'addedAt') {
|
|
||||||
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
|
|
||||||
} else if (sortBy === 'size') {
|
|
||||||
return [[Sequelize.literal('libraryItem.size'), dir]]
|
|
||||||
} else if (sortBy === 'birthtimeMs') {
|
|
||||||
return [[Sequelize.literal('libraryItem.birthtime'), dir]]
|
|
||||||
} else if (sortBy === 'mtimeMs') {
|
|
||||||
return [[Sequelize.literal('libraryItem.mtime'), dir]]
|
|
||||||
} else if (sortBy === 'media.duration') {
|
|
||||||
return [['duration', dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.publishedYear') {
|
|
||||||
return [['publishedYear', dir]]
|
|
||||||
} else if (sortBy === 'media.metadata.authorNameLF') {
|
|
||||||
return [] // TODO: Handle author filter
|
|
||||||
} else if (sortBy === 'media.metadata.authorName') {
|
|
||||||
return [] // TODO: Handle author filter
|
|
||||||
} else if (sortBy === 'media.metadata.title') {
|
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
|
||||||
return [['titleIgnorePrefix', dir]]
|
|
||||||
} else {
|
|
||||||
return [['title', dir]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
|
|
||||||
async getLibraryItemsWithNoSeries(libraryId, sortBy, sortDesc, limit, offset) {
|
|
||||||
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
|
||||||
where: {
|
|
||||||
'$series.id$': null
|
|
||||||
},
|
|
||||||
distinct: true,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.models.libraryItem,
|
|
||||||
required: true,
|
|
||||||
where: {
|
|
||||||
libraryId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.models.series,
|
|
||||||
attributes: ['id', 'name'],
|
|
||||||
through: {
|
|
||||||
attributes: ['sequence']
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.models.bookAuthor,
|
|
||||||
attributes: ['authorId'],
|
|
||||||
include: {
|
|
||||||
model: Database.models.author,
|
|
||||||
attributes: ['id', 'name']
|
|
||||||
},
|
|
||||||
separate: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: this.getOrder(sortBy, sortDesc),
|
|
||||||
subQuery: false,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
})
|
|
||||||
|
|
||||||
const libraryItems = books.map((bookExpanded) => {
|
|
||||||
const libraryItem = bookExpanded.libraryItem.toJSON()
|
|
||||||
const book = bookExpanded.toJSON()
|
|
||||||
delete book.libraryItem
|
|
||||||
|
|
||||||
book.authors = []
|
|
||||||
if (book.bookAuthors?.length) {
|
|
||||||
book.bookAuthors.forEach((ba) => {
|
|
||||||
if (ba.author) {
|
|
||||||
book.authors.push(ba.author)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
delete book.bookAuthors
|
|
||||||
|
|
||||||
libraryItem.media = book
|
|
||||||
|
|
||||||
return libraryItem
|
|
||||||
})
|
|
||||||
Logger.debug('Found', libraryItems.length, 'library items', 'total=', count)
|
|
||||||
return {
|
|
||||||
libraryItems,
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user