mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-01 16:49:38 +00:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
ef74919f12
78 changed files with 2266 additions and 418 deletions
|
|
@ -5,7 +5,7 @@ const http = require('http')
|
|||
const util = require('util')
|
||||
const fs = require('./libs/fsExtra')
|
||||
const fileUpload = require('./libs/expressFileupload')
|
||||
const cookieParser = require("cookie-parser")
|
||||
const cookieParser = require('cookie-parser')
|
||||
|
||||
const { version } = require('../package.json')
|
||||
|
||||
|
|
@ -180,15 +180,15 @@ class Server {
|
|||
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
|
||||
* The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
|
||||
* @see https://ionicframework.com/docs/troubleshooting/cors
|
||||
*
|
||||
* Running in development allows cors to allow testing the mobile apps in the browser
|
||||
*
|
||||
* Running in development allows cors to allow testing the mobile apps in the browser
|
||||
*/
|
||||
app.use((req, res, next) => {
|
||||
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
||||
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
||||
if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) {
|
||||
if (Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
|
||||
res.header('Access-Control-Allow-Origin', req.get('origin'))
|
||||
res.header("Access-Control-Allow-Methods", 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||
res.header('Access-Control-Allow-Headers', '*')
|
||||
res.header('Access-Control-Allow-Credentials', true)
|
||||
if (req.method === 'OPTIONS') {
|
||||
|
|
@ -203,15 +203,17 @@ class Server {
|
|||
// parse cookies in requests
|
||||
app.use(cookieParser())
|
||||
// enable express-session
|
||||
app.use(expressSession({
|
||||
secret: global.ServerSettings.tokenSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
// also send the cookie if were are not on https (not every use has https)
|
||||
secure: false
|
||||
},
|
||||
}))
|
||||
app.use(
|
||||
expressSession({
|
||||
secret: global.ServerSettings.tokenSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
// also send the cookie if were are not on https (not every use has https)
|
||||
secure: false
|
||||
}
|
||||
})
|
||||
)
|
||||
// init passport.js
|
||||
app.use(passport.initialize())
|
||||
// register passport in express-session
|
||||
|
|
@ -225,14 +227,16 @@ class Server {
|
|||
|
||||
this.server = http.createServer(app)
|
||||
|
||||
router.use(fileUpload({
|
||||
defCharset: 'utf8',
|
||||
defParamCharset: 'utf8',
|
||||
useTempFiles: true,
|
||||
tempFileDir: Path.join(global.MetadataPath, 'tmp')
|
||||
}))
|
||||
router.use(express.urlencoded({ extended: true, limit: "5mb" }))
|
||||
router.use(express.json({ limit: "5mb" }))
|
||||
router.use(
|
||||
fileUpload({
|
||||
defCharset: 'utf8',
|
||||
defParamCharset: 'utf8',
|
||||
useTempFiles: true,
|
||||
tempFileDir: Path.join(global.MetadataPath, 'tmp')
|
||||
})
|
||||
)
|
||||
router.use(express.urlencoded({ extended: true, limit: '5mb' }))
|
||||
router.use(express.json({ limit: '5mb' }))
|
||||
|
||||
// Static path to generated nuxt
|
||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||
|
|
@ -361,7 +365,7 @@ class Server {
|
|||
const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
|
||||
where: {
|
||||
id: {
|
||||
[Sequelize.Op.in]: mediaProgressToRemove.map(mp => mp.id)
|
||||
[Sequelize.Op.in]: mediaProgressToRemove.map((mp) => mp.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -375,15 +379,18 @@ class Server {
|
|||
for (const _user of users) {
|
||||
let hasUpdated = false
|
||||
if (_user.seriesHideFromContinueListening.length) {
|
||||
const seriesHiding = (await Database.seriesModel.findAll({
|
||||
where: {
|
||||
id: _user.seriesHideFromContinueListening
|
||||
},
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})).map(se => se.id)
|
||||
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
||||
if (!seriesHiding.includes(seriesId)) { // Series removed
|
||||
const seriesHiding = (
|
||||
await Database.seriesModel.findAll({
|
||||
where: {
|
||||
id: _user.seriesHideFromContinueListening
|
||||
},
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})
|
||||
).map((se) => se.id)
|
||||
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter((seriesId) => {
|
||||
if (!seriesHiding.includes(seriesId)) {
|
||||
// Series removed
|
||||
hasUpdated = true
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const naturalSort = createNewSortInstance({
|
|||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
class AuthorController {
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
async findOne(req, res) {
|
||||
const include = (req.query.include || '').split(',')
|
||||
|
|
@ -32,7 +32,6 @@ class AuthorController {
|
|||
authorJson.libraryItems.forEach((li) => {
|
||||
if (li.media.metadata.series) {
|
||||
li.media.metadata.series.forEach((series) => {
|
||||
|
||||
const itemWithSeries = li.toJSONMinified()
|
||||
itemWithSeries.media.metadata.series = series
|
||||
|
||||
|
|
@ -50,14 +49,14 @@ class AuthorController {
|
|||
})
|
||||
// Sort series items
|
||||
for (const key in seriesMap) {
|
||||
seriesMap[key].items = naturalSort(seriesMap[key].items).asc(li => li.media.metadata.series.sequence)
|
||||
seriesMap[key].items = naturalSort(seriesMap[key].items).asc((li) => li.media.metadata.series.sequence)
|
||||
}
|
||||
|
||||
authorJson.series = Object.values(seriesMap)
|
||||
}
|
||||
|
||||
// Minify library items
|
||||
authorJson.libraryItems = authorJson.libraryItems.map(li => li.toJSONMinified())
|
||||
authorJson.libraryItems = authorJson.libraryItems.map((li) => li.toJSONMinified())
|
||||
}
|
||||
|
||||
return res.json(authorJson)
|
||||
|
|
@ -91,7 +90,8 @@ class AuthorController {
|
|||
if (existingAuthor) {
|
||||
const bookAuthorsToCreate = []
|
||||
const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
|
||||
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||
itemsWithAuthor.forEach((libraryItem) => {
|
||||
// Replace old author with merging author for each book
|
||||
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||
bookAuthorsToCreate.push({
|
||||
bookId: libraryItem.media.id,
|
||||
|
|
@ -101,7 +101,10 @@ class AuthorController {
|
|||
if (itemsWithAuthor.length) {
|
||||
await Database.removeBulkBookAuthors(req.author.id) // Remove all old BookAuthor
|
||||
await Database.createBulkBookAuthors(bookAuthorsToCreate) // Create all new BookAuthor
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
SocketAuthority.emitter(
|
||||
'items_updated',
|
||||
itemsWithAuthor.map((li) => li.toJSONExpanded())
|
||||
)
|
||||
}
|
||||
|
||||
// Remove old author
|
||||
|
|
@ -118,7 +121,8 @@ class AuthorController {
|
|||
author: existingAuthor.toJSON(),
|
||||
merged: true
|
||||
})
|
||||
} else { // Regular author update
|
||||
} else {
|
||||
// Regular author update
|
||||
if (req.author.update(payload)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
|
|
@ -127,12 +131,16 @@ class AuthorController {
|
|||
req.author.updatedAt = Date.now()
|
||||
|
||||
const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
|
||||
if (authorNameUpdate) { // Update author name on all books
|
||||
itemsWithAuthor.forEach(libraryItem => {
|
||||
if (authorNameUpdate) {
|
||||
// Update author name on all books
|
||||
itemsWithAuthor.forEach((libraryItem) => {
|
||||
libraryItem.media.metadata.updateAuthor(req.author)
|
||||
})
|
||||
if (itemsWithAuthor.length) {
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
SocketAuthority.emitter(
|
||||
'items_updated',
|
||||
itemsWithAuthor.map((li) => li.toJSONExpanded())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,9 +158,9 @@ class AuthorController {
|
|||
/**
|
||||
* DELETE: /api/authors/:id
|
||||
* Remove author from all books and delete
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async delete(req, res) {
|
||||
Logger.info(`[AuthorController] Removing author "${req.author.name}"`)
|
||||
|
|
@ -174,9 +182,9 @@ class AuthorController {
|
|||
/**
|
||||
* POST: /api/authors/:id/image
|
||||
* Upload author image from web URL
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async uploadImage(req, res) {
|
||||
if (!req.user.canUpload) {
|
||||
|
|
@ -206,6 +214,7 @@ class AuthorController {
|
|||
}
|
||||
|
||||
req.author.imagePath = result.path
|
||||
req.author.updatedAt = Date.now()
|
||||
await Database.authorModel.updateFromOld(req.author)
|
||||
|
||||
const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
|
||||
|
|
@ -218,9 +227,9 @@ class AuthorController {
|
|||
/**
|
||||
* DELETE: /api/authors/:id/image
|
||||
* Remove author image & delete image file
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async deleteImage(req, res) {
|
||||
if (!req.author.imagePath) {
|
||||
|
|
@ -292,10 +301,14 @@ class AuthorController {
|
|||
|
||||
// GET api/authors/:id/image
|
||||
async getImage(req, res) {
|
||||
const { query: { width, height, format, raw }, author } = req
|
||||
const {
|
||||
query: { width, height, format, raw },
|
||||
author
|
||||
} = req
|
||||
|
||||
if (raw) { // any value
|
||||
if (!author.imagePath || !await fs.pathExists(author.imagePath)) {
|
||||
if (raw) {
|
||||
// any value
|
||||
if (!author.imagePath || !(await fs.pathExists(author.imagePath))) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
|
|
@ -326,4 +339,4 @@ class AuthorController {
|
|||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new AuthorController()
|
||||
module.exports = new AuthorController()
|
||||
|
|
|
|||
|
|
@ -605,12 +605,12 @@ class LibraryController {
|
|||
}
|
||||
|
||||
if (req.library.isBook) {
|
||||
const authors = await authorFilters.getAuthorsWithCount(req.library.id)
|
||||
const authors = await authorFilters.getAuthorsWithCount(req.library.id, 10)
|
||||
const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
|
||||
const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
|
||||
const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)
|
||||
|
||||
stats.totalAuthors = authors.length
|
||||
stats.totalAuthors = await authorFilters.getAuthorsTotalCount(req.library.id)
|
||||
stats.authorsWithCount = authors
|
||||
stats.totalGenres = genres.length
|
||||
stats.genresWithCount = genres
|
||||
|
|
|
|||
|
|
@ -3,45 +3,61 @@ const Database = require('../../Database')
|
|||
|
||||
module.exports = {
|
||||
/**
|
||||
* Get authors with count of num books
|
||||
* @param {string} libraryId
|
||||
* @returns {{id:string, name:string, count:number}}
|
||||
* Get authors total count
|
||||
*
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<number>} count
|
||||
*/
|
||||
async getAuthorsWithCount(libraryId) {
|
||||
const authors = await Database.authorModel.findAll({
|
||||
where: [
|
||||
{
|
||||
libraryId
|
||||
},
|
||||
Sequelize.where(Sequelize.literal('count'), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
})
|
||||
],
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'count']
|
||||
],
|
||||
order: [
|
||||
['count', 'DESC']
|
||||
]
|
||||
async getAuthorsTotalCount(libraryId) {
|
||||
const authorsCount = await Database.authorModel.count({
|
||||
where: {
|
||||
libraryId: libraryId
|
||||
}
|
||||
})
|
||||
return authors.map(au => {
|
||||
return authorsCount
|
||||
},
|
||||
|
||||
/**
|
||||
* Get authors with count of num books
|
||||
*
|
||||
* @param {string} libraryId
|
||||
* @param {number} limit
|
||||
* @returns {Promise<{id:string, name:string, count:number}>}
|
||||
*/
|
||||
async getAuthorsWithCount(libraryId, limit) {
|
||||
const authors = await Database.bookAuthorModel.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Database.authorModel,
|
||||
as: 'author', // Use the correct alias as defined in your associations
|
||||
attributes: ['name'],
|
||||
where: {
|
||||
libraryId: libraryId
|
||||
}
|
||||
}
|
||||
],
|
||||
attributes: ['authorId', [Sequelize.fn('COUNT', Sequelize.col('authorId')), 'count']],
|
||||
group: ['authorId', 'author.id'], // Include 'author.id' to satisfy GROUP BY with JOIN
|
||||
order: [[Sequelize.literal('count'), 'DESC']],
|
||||
limit: limit
|
||||
})
|
||||
return authors.map((au) => {
|
||||
return {
|
||||
id: au.id,
|
||||
name: au.name,
|
||||
count: au.dataValues.count
|
||||
id: au.authorId,
|
||||
name: au.author.name,
|
||||
count: au.get('count') // Use get method to access aliased attributes
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Search authors
|
||||
* @param {string} libraryId
|
||||
* @param {string} query
|
||||
*
|
||||
* @param {string} libraryId
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {object[]} oldAuthor with numBooks
|
||||
* @returns {Promise<Object[]>} oldAuthor with numBooks
|
||||
*/
|
||||
async search(libraryId, query, limit, offset) {
|
||||
const authors = await Database.authorModel.findAll({
|
||||
|
|
@ -52,9 +68,7 @@ module.exports = {
|
|||
libraryId
|
||||
},
|
||||
attributes: {
|
||||
include: [
|
||||
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
|
||||
]
|
||||
include: [[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']]
|
||||
},
|
||||
limit,
|
||||
offset
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const { asciiOnlyToLowerCase } = require('../index')
|
|||
module.exports = {
|
||||
/**
|
||||
* User permissions to restrict books for explicit content & tags
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @returns {{ bookWhere:Sequelize.WhereOptions, replacements:object }}
|
||||
*/
|
||||
getUserPermissionBookWhereQuery(user) {
|
||||
|
|
@ -25,9 +25,11 @@ module.exports = {
|
|||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0))
|
||||
} else {
|
||||
bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}))
|
||||
bookWhere.push(
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
@ -39,8 +41,8 @@ module.exports = {
|
|||
/**
|
||||
* When collapsing series and filtering by progress
|
||||
* different where options are required
|
||||
*
|
||||
* @param {string} value
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {Sequelize.WhereOptions}
|
||||
*/
|
||||
getCollapseSeriesMediaProgressFilter(value) {
|
||||
|
|
@ -90,8 +92,8 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get where options for Book model
|
||||
* @param {string} group
|
||||
* @param {[string]} value
|
||||
* @param {string} group
|
||||
* @param {[string]} value
|
||||
* @returns {object} { Sequelize.WhereOptions, string[] }
|
||||
*/
|
||||
getMediaGroupQuery(group, value) {
|
||||
|
|
@ -170,7 +172,7 @@ module.exports = {
|
|||
Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 0),
|
||||
{
|
||||
'$mediaProgresses.isFinished$': true,
|
||||
'ebookFile': {
|
||||
ebookFile: {
|
||||
[Sequelize.Op.not]: null
|
||||
}
|
||||
}
|
||||
|
|
@ -232,8 +234,8 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get sequelize order
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @param {boolean} collapseseries
|
||||
* @returns {Sequelize.order}
|
||||
*/
|
||||
|
|
@ -278,18 +280,14 @@ module.exports = {
|
|||
* When collapsing series get first book in each series
|
||||
* to know which books to exclude from primary query.
|
||||
* Additionally use this query to get the number of books in each series
|
||||
*
|
||||
* @param {Sequelize.ModelStatic} bookFindOptions
|
||||
* @param {Sequelize.WhereOptions} seriesWhere
|
||||
*
|
||||
* @param {Sequelize.ModelStatic} bookFindOptions
|
||||
* @param {Sequelize.WhereOptions} seriesWhere
|
||||
* @returns {object} { booksToExclude, bookSeriesToInclude }
|
||||
*/
|
||||
async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) {
|
||||
const allSeries = await Database.seriesModel.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
[Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks']
|
||||
],
|
||||
attributes: ['id', 'name', [Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks']],
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
where: seriesWhere,
|
||||
|
|
@ -304,23 +302,22 @@ module.exports = {
|
|||
required: true
|
||||
}
|
||||
],
|
||||
order: [
|
||||
Sequelize.literal('CAST(`books.bookSeries.sequence` AS FLOAT) ASC NULLS LAST')
|
||||
]
|
||||
order: [Sequelize.literal('CAST(`books.bookSeries.sequence` AS FLOAT) ASC NULLS LAST')]
|
||||
})
|
||||
const bookSeriesToInclude = []
|
||||
const booksToInclude = []
|
||||
let booksToExclude = []
|
||||
allSeries.forEach(s => {
|
||||
allSeries.forEach((s) => {
|
||||
let found = false
|
||||
for (let book of s.books) {
|
||||
if (!found && !booksToInclude.includes(book.id)) {
|
||||
booksToInclude.push(book.id)
|
||||
bookSeriesToInclude.push({
|
||||
id: book.bookSeries.id,
|
||||
numBooks: s.dataValues.numBooks
|
||||
numBooks: s.dataValues.numBooks,
|
||||
libraryItemIds: s.books?.map((b) => b.libraryItem.id) || []
|
||||
})
|
||||
booksToExclude = booksToExclude.filter(bid => bid !== book.id)
|
||||
booksToExclude = booksToExclude.filter((bid) => bid !== book.id)
|
||||
found = true
|
||||
} else if (!booksToExclude.includes(book.id) && !booksToInclude.includes(book.id)) {
|
||||
booksToExclude.push(book.id)
|
||||
|
|
@ -332,16 +329,16 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get library items for book media type using filter and sort
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @param {[oldUser]} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {boolean} collapseseries
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {boolean} isHomePage for home page shelves
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
|
|
@ -363,15 +360,11 @@ module.exports = {
|
|||
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']
|
||||
]
|
||||
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']
|
||||
]
|
||||
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']]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,9 +379,7 @@ module.exports = {
|
|||
model: Database.seriesModel,
|
||||
attributes: ['id', 'name', 'nameIgnorePrefix']
|
||||
},
|
||||
order: [
|
||||
['createdAt', 'ASC']
|
||||
],
|
||||
order: [['createdAt', 'ASC']],
|
||||
separate: true
|
||||
}
|
||||
|
||||
|
|
@ -399,9 +390,7 @@ module.exports = {
|
|||
model: Database.authorModel,
|
||||
attributes: ['id', 'name']
|
||||
},
|
||||
order: [
|
||||
['createdAt', 'ASC']
|
||||
],
|
||||
order: [['createdAt', 'ASC']],
|
||||
separate: true
|
||||
}
|
||||
|
||||
|
|
@ -427,7 +416,7 @@ module.exports = {
|
|||
}
|
||||
} else if (filterGroup === 'ebooks' && filterValue === 'no-supplementary') {
|
||||
libraryItemWhere['libraryFiles'] = {
|
||||
[Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`),
|
||||
[Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`)
|
||||
}
|
||||
} else if (filterGroup === 'missing' && filterValue === 'authors') {
|
||||
authorInclude = {
|
||||
|
|
@ -496,7 +485,7 @@ module.exports = {
|
|||
})
|
||||
} else if (filterGroup === 'recent') {
|
||||
libraryItemWhere['createdAt'] = {
|
||||
[Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago
|
||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -551,9 +540,9 @@ module.exports = {
|
|||
// When collapsing series and sorting by title then use the series name instead of the book title
|
||||
// for this set an attribute "display_title" to use in sorting
|
||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title'])
|
||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title'])
|
||||
} else {
|
||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title'])
|
||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title'])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,15 +587,16 @@ module.exports = {
|
|||
|
||||
// For showing details of collapsed series
|
||||
if (collapseseries && book.bookSeries?.length) {
|
||||
const collapsedSeries = book.bookSeries.find(bs => collapseSeriesBookSeries.some(cbs => cbs.id === bs.id))
|
||||
const collapsedSeries = book.bookSeries.find((bs) => collapseSeriesBookSeries.some((cbs) => cbs.id === bs.id))
|
||||
if (collapsedSeries) {
|
||||
const collapseSeriesObj = collapseSeriesBookSeries.find(csbs => csbs.id === collapsedSeries.id)
|
||||
const collapseSeriesObj = collapseSeriesBookSeries.find((csbs) => csbs.id === collapsedSeries.id)
|
||||
libraryItem.collapsedSeries = {
|
||||
id: collapsedSeries.series.id,
|
||||
name: collapsedSeries.series.name,
|
||||
nameIgnorePrefix: collapsedSeries.series.nameIgnorePrefix,
|
||||
sequence: collapsedSeries.sequence,
|
||||
numBooks: collapseSeriesObj?.numBooks || 0
|
||||
numBooks: collapseSeriesObj?.numBooks || 0,
|
||||
libraryItemIds: collapseSeriesObj?.libraryItemIds || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -633,11 +623,11 @@ module.exports = {
|
|||
* 2. Has no books in progress
|
||||
* 3. Has at least 1 unfinished book
|
||||
* TODO: Reduce queries
|
||||
* @param {import('../../objects/Library')} library
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {import('../../objects/Library')} library
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {{ libraryItems:import('../../models/LibraryItem')[], count:number }}
|
||||
*/
|
||||
async getContinueSeriesLibraryItems(library, user, include, limit, offset) {
|
||||
|
|
@ -655,15 +645,13 @@ module.exports = {
|
|||
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
|
||||
bookWhere.push(...userPermissionBookWhere.bookWhere)
|
||||
|
||||
let includeAttributes = [
|
||||
[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'],
|
||||
]
|
||||
let includeAttributes = [[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress']]
|
||||
let booksNotFinishedQuery = `SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL)`
|
||||
|
||||
if (library.settings.onlyShowLaterBooksInContinueSeries) {
|
||||
const maxSequenceQuery = `(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)`
|
||||
includeAttributes.push([Sequelize.literal(`${maxSequenceQuery}`), 'maxSequence'])
|
||||
|
||||
|
||||
booksNotFinishedQuery = booksNotFinishedQuery + ` AND CAST(bs.sequence as FLOAT) > ${maxSequenceQuery}`
|
||||
}
|
||||
|
||||
|
|
@ -699,9 +687,7 @@ module.exports = {
|
|||
attributes: ['bookId', 'sequence'],
|
||||
separate: true,
|
||||
subQuery: false,
|
||||
order: [
|
||||
[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]
|
||||
],
|
||||
order: [[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]],
|
||||
where: {
|
||||
'$book.mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
|
|
@ -731,44 +717,44 @@ module.exports = {
|
|||
]
|
||||
}
|
||||
},
|
||||
order: [
|
||||
[Sequelize.literal('recent_progress DESC')]
|
||||
],
|
||||
order: [[Sequelize.literal('recent_progress DESC')]],
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
|
||||
const libraryItems = series.map(s => {
|
||||
if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series
|
||||
const libraryItems = series
|
||||
.map((s) => {
|
||||
if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series
|
||||
|
||||
let bookIndex = 0
|
||||
// if the library setting is toggled, only show later entries in series, otherwise skip
|
||||
if (library.settings.onlyShowLaterBooksInContinueSeries) {
|
||||
bookIndex = s.bookSeries.findIndex(function (b) {
|
||||
return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence
|
||||
})
|
||||
if (bookIndex === -1) {
|
||||
// no later books than maxSequence
|
||||
return null
|
||||
let bookIndex = 0
|
||||
// if the library setting is toggled, only show later entries in series, otherwise skip
|
||||
if (library.settings.onlyShowLaterBooksInContinueSeries) {
|
||||
bookIndex = s.bookSeries.findIndex(function (b) {
|
||||
return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence
|
||||
})
|
||||
if (bookIndex === -1) {
|
||||
// no later books than maxSequence
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON()
|
||||
const book = s.bookSeries[bookIndex].book.toJSON()
|
||||
delete book.libraryItem
|
||||
libraryItem.series = {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
sequence: s.bookSeries[bookIndex].sequence
|
||||
}
|
||||
if (libraryItem.feeds?.length) {
|
||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||
}
|
||||
libraryItem.media = book
|
||||
return libraryItem
|
||||
}).filter(s => s)
|
||||
const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON()
|
||||
const book = s.bookSeries[bookIndex].book.toJSON()
|
||||
delete book.libraryItem
|
||||
libraryItem.series = {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
sequence: s.bookSeries[bookIndex].sequence
|
||||
}
|
||||
if (libraryItem.feeds?.length) {
|
||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||
}
|
||||
libraryItem.media = book
|
||||
return libraryItem
|
||||
})
|
||||
.filter((s) => s)
|
||||
|
||||
return {
|
||||
libraryItems,
|
||||
|
|
@ -780,10 +766,10 @@ module.exports = {
|
|||
* Get book library items for the "Discover" shelf
|
||||
* Random selection of books that are not started
|
||||
* - only includes the first book of a not-started series
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} {libraryItems:LibraryItem, count:number}
|
||||
*/
|
||||
async getDiscoverLibraryItems(libraryId, user, include, limit) {
|
||||
|
|
@ -811,9 +797,7 @@ module.exports = {
|
|||
model: Database.bookModel,
|
||||
where: userPermissionBookWhere.bookWhere
|
||||
},
|
||||
order: [
|
||||
[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]
|
||||
],
|
||||
order: [[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]],
|
||||
limit: 1
|
||||
},
|
||||
subQuery: false,
|
||||
|
|
@ -821,7 +805,7 @@ module.exports = {
|
|||
order: Database.sequelize.random()
|
||||
})
|
||||
|
||||
const booksFromSeriesToInclude = seriesNotStarted.map(se => se.bookSeries?.[0]?.bookId).filter(bid => bid)
|
||||
const booksFromSeriesToInclude = seriesNotStarted.map((se) => se.bookSeries?.[0]?.bookId).filter((bid) => bid)
|
||||
|
||||
// optional include rssFeed
|
||||
const libraryItemIncludes = []
|
||||
|
|
@ -913,7 +897,7 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get book library items in a collection
|
||||
* @param {oldCollection} collection
|
||||
* @param {oldCollection} collection
|
||||
* @returns {Promise<LibraryItem[]>}
|
||||
*/
|
||||
async getLibraryItemsForCollection(collection) {
|
||||
|
|
@ -957,22 +941,22 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get library items for series
|
||||
* @param {import('../../objects/entities/Series')} oldSeries
|
||||
* @param {import('../../objects/user/User')} [oldUser]
|
||||
* @param {import('../../objects/entities/Series')} oldSeries
|
||||
* @param {import('../../objects/user/User')} [oldUser]
|
||||
* @returns {Promise<import('../../objects/LibraryItem')[]>}
|
||||
*/
|
||||
async getLibraryItemsForSeries(oldSeries, oldUser) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null)
|
||||
return libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li))
|
||||
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
|
||||
},
|
||||
|
||||
/**
|
||||
* Search books, authors, series
|
||||
* @param {import('../../objects/user/User')} oldUser
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}}
|
||||
*/
|
||||
async search(oldUser, oldLibrary, query, limit, offset) {
|
||||
|
|
@ -1151,7 +1135,7 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Genres with num books
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {{genre:string, count:number}[]}
|
||||
*/
|
||||
async getGenresWithCount(libraryId) {
|
||||
|
|
@ -1173,7 +1157,7 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get stats for book library
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
|
||||
*/
|
||||
async getBookLibraryStats(libraryId) {
|
||||
|
|
@ -1187,8 +1171,8 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Get longest books in library
|
||||
* @param {string} libraryId
|
||||
* @param {number} limit
|
||||
* @param {string} libraryId
|
||||
* @param {number} limit
|
||||
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
|
||||
*/
|
||||
async getLongestBooks(libraryId, limit) {
|
||||
|
|
@ -1201,12 +1185,10 @@ module.exports = {
|
|||
libraryId
|
||||
}
|
||||
},
|
||||
order: [
|
||||
['duration', 'DESC']
|
||||
],
|
||||
order: [['duration', 'DESC']],
|
||||
limit
|
||||
})
|
||||
return books.map(book => {
|
||||
return books.map((book) => {
|
||||
return {
|
||||
id: book.libraryItem.id,
|
||||
title: book.title,
|
||||
|
|
@ -1214,4 +1196,4 @@ module.exports = {
|
|||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue