Update bookSeries & bookAuthors table to include createdAt timestamp

This commit is contained in:
advplyr 2023-07-29 17:25:11 -05:00
parent 4dbe8d29d9
commit 4d0acb30ba
9 changed files with 447 additions and 407 deletions

View file

@ -1,154 +1,21 @@
const { Op, literal, col, fn, where } = require('sequelize')
const Database = require('../../Database')
const libraryItemsSeriesFilters = require('./libraryItemsSeriesFilters')
const libraryItemsProgressFilters = require('./libraryItemsProgressFilters')
const Logger = require('../../Logger')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
module.exports = {
decode(text) {
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) {
const libraryItemModel = Database.models.libraryItem
let mediaWhereQuery = null
let mediaAttributes = null
let itemWhereQuery = {
libraryId
let filterValue = null
let filterGroup = null
if (filterBy) {
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
filterGroup = group || filterBy
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
}
const itemIncludes = []
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 }
// TODO: Handle podcast filters
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset)
}
}

View 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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}