2024-08-12 00:01:25 +02:00
const { Request , Response , NextFunction } = require ( 'express' )
2023-08-14 00:45:53 +02:00
const Sequelize = require ( 'sequelize' )
2022-03-14 15:56:24 +01:00
const Path = require ( 'path' )
2022-07-06 02:53:01 +02:00
const fs = require ( '../libs/fsExtra' )
2021-11-22 03:00:40 +01:00
const Logger = require ( '../Logger' )
2022-11-24 22:53:58 +01:00
const SocketAuthority = require ( '../SocketAuthority' )
2021-12-01 03:02:40 +01:00
const libraryHelpers = require ( '../utils/libraryHelpers' )
2023-08-13 22:10:26 +02:00
const libraryItemsBookFilters = require ( '../utils/queries/libraryItemsBookFilters' )
2023-08-14 00:45:53 +02:00
const libraryItemFilters = require ( '../utils/queries/libraryItemFilters' )
2023-08-19 00:08:34 +02:00
const seriesFilters = require ( '../utils/queries/seriesFilters' )
2023-09-04 00:51:58 +02:00
const fileUtils = require ( '../utils/fileUtils' )
2023-10-06 00:00:40 +02:00
const { createNewSortInstance } = require ( '../libs/fastSort' )
2021-12-26 23:17:10 +01:00
const naturalSort = createNewSortInstance ( {
comparer : new Intl . Collator ( undefined , { numeric : true , sensitivity : 'base' } ) . compare
} )
2023-07-05 01:14:44 +02:00
2023-09-04 00:51:58 +02:00
const LibraryScanner = require ( '../scanner/LibraryScanner' )
2023-09-07 00:48:50 +02:00
const Scanner = require ( '../scanner/Scanner' )
2023-07-05 01:14:44 +02:00
const Database = require ( '../Database' )
2023-08-13 22:10:26 +02:00
const libraryFilters = require ( '../utils/queries/libraryFilters' )
2023-08-19 21:49:06 +02:00
const libraryItemsPodcastFilters = require ( '../utils/queries/libraryItemsPodcastFilters' )
2023-08-19 23:53:33 +02:00
const authorFilters = require ( '../utils/queries/authorFilters' )
2023-07-05 01:14:44 +02:00
2024-08-12 00:01:25 +02:00
/ * *
* @ typedef RequestUserObject
* @ property { import ( '../models/User' ) } user
*
* @ typedef { Request & RequestUserObject } RequestWithUser
2024-08-24 22:38:15 +02:00
*
* @ typedef RequestEntityObject
* @ property { import ( '../models/Library' ) } library
*
* @ typedef { RequestWithUser & RequestEntityObject } LibraryControllerRequest
2024-08-12 00:01:25 +02:00
* /
2021-11-22 03:00:40 +01:00
class LibraryController {
2024-05-15 00:24:39 +02:00
constructor ( ) { }
2021-11-22 03:00:40 +01:00
2024-08-12 00:01:25 +02:00
/ * *
* POST : / a p i / l i b r a r i e s
* Create a new library
*
* @ param { RequestWithUser } req
* @ param { Response } res
* /
2021-11-22 03:00:40 +01:00
async create ( req , res ) {
2024-08-23 00:39:28 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user " ${ req . user . username } " attempted to create library ` )
return res . sendStatus ( 403 )
}
// Validation
if ( ! req . body . name || typeof req . body . name !== 'string' ) {
return res . status ( 400 ) . send ( 'Invalid request. Name must be a string' )
}
if (
! Array . isArray ( req . body . folders ) ||
req . body . folders . some ( ( f ) => {
// Old model uses fullPath and new model will use path. Support both for now
const path = f ? . fullPath || f ? . path
return ! path || typeof path !== 'string'
} )
) {
return res . status ( 400 ) . send ( 'Invalid request. Folders must be a non-empty array of objects with path string' )
}
const optionalStringFields = [ 'mediaType' , 'icon' , 'provider' ]
for ( const field of optionalStringFields ) {
if ( req . body [ field ] && typeof req . body [ field ] !== 'string' ) {
return res . status ( 400 ) . send ( ` Invalid request. ${ field } must be a string ` )
}
}
if ( req . body . settings && ( typeof req . body . settings !== 'object' || Array . isArray ( req . body . settings ) ) ) {
return res . status ( 400 ) . send ( 'Invalid request. Settings must be an object' )
}
const mediaType = req . body . mediaType || 'book'
2022-12-01 00:32:59 +01:00
const newLibraryPayload = {
2024-08-23 00:39:28 +02:00
name : req . body . name ,
provider : req . body . provider || 'google' ,
mediaType ,
icon : req . body . icon || 'database' ,
settings : Database . libraryModel . getDefaultLibrarySettingsForMediaType ( mediaType )
2021-11-22 03:00:40 +01:00
}
2024-08-23 00:39:28 +02:00
// Validate settings
if ( req . body . settings ) {
for ( const key in req . body . settings ) {
if ( newLibraryPayload . settings [ key ] !== undefined ) {
if ( key === 'metadataPrecedence' ) {
if ( ! Array . isArray ( req . body . settings [ key ] ) ) {
return res . status ( 400 ) . send ( 'Invalid request. Settings "metadataPrecedence" must be an array' )
}
newLibraryPayload . settings [ key ] = [ ... req . body . settings [ key ] ]
} else if ( key === 'autoScanCronExpression' || key === 'podcastSearchRegion' ) {
if ( ! req . body . settings [ key ] ) continue
if ( typeof req . body . settings [ key ] !== 'string' ) {
return res . status ( 400 ) . send ( ` Invalid request. Settings " ${ key } " must be a string ` )
}
newLibraryPayload . settings [ key ] = req . body . settings [ key ]
} else {
if ( typeof req . body . settings [ key ] !== typeof newLibraryPayload . settings [ key ] ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be of type ${ typeof newLibraryPayload . settings [ key ] } ` )
}
newLibraryPayload . settings [ key ] = req . body . settings [ key ]
}
}
}
2021-11-22 03:00:40 +01:00
}
2024-02-11 23:48:16 +01:00
// Validate that the custom provider exists if given any
2024-08-23 00:39:28 +02:00
if ( newLibraryPayload . provider . startsWith ( 'custom-' ) ) {
2024-05-15 00:24:39 +02:00
if ( ! ( await Database . customMetadataProviderModel . checkExistsBySlug ( newLibraryPayload . provider ) ) ) {
2024-02-11 23:48:16 +01:00
Logger . error ( ` [LibraryController] Custom metadata provider " ${ newLibraryPayload . provider } " does not exist ` )
2024-08-23 00:39:28 +02:00
return res . status ( 400 ) . send ( 'Invalid request. Custom metadata provider does not exist' )
2024-02-11 23:48:16 +01:00
}
}
2022-03-19 12:41:54 +01:00
// Validate folder paths exist or can be created & resolve rel paths
// returns 400 if a folder fails to access
2024-08-23 00:39:28 +02:00
newLibraryPayload . libraryFolders = req . body . folders . map ( ( f ) => {
const fpath = f . fullPath || f . path
f . path = fileUtils . filePathToPOSIX ( Path . resolve ( fpath ) )
2022-03-19 12:41:54 +01:00
return f
} )
2024-08-23 00:39:28 +02:00
for ( const folder of newLibraryPayload . libraryFolders ) {
2022-04-21 00:49:34 +02:00
try {
2024-08-23 00:39:28 +02:00
// Create folder if it doesn't exist
await fs . ensureDir ( folder . path )
2022-04-21 00:49:34 +02:00
} catch ( error ) {
2024-08-23 00:39:28 +02:00
Logger . error ( ` [LibraryController] Failed to ensure folder dir " ${ folder . path } " ` , error )
return res . status ( 400 ) . send ( ` Invalid request. Invalid folder directory " ${ folder . path } " ` )
2022-03-19 12:41:54 +01:00
}
}
2024-08-23 00:39:28 +02:00
// Set display order
2023-08-20 20:34:03 +02:00
let currentLargestDisplayOrder = await Database . libraryModel . getMaxDisplayOrder ( )
2023-07-22 21:25:20 +02:00
if ( isNaN ( currentLargestDisplayOrder ) ) currentLargestDisplayOrder = 0
newLibraryPayload . displayOrder = currentLargestDisplayOrder + 1
2024-08-23 00:39:28 +02:00
// Create library with libraryFolders
const library = await Database . libraryModel
. create ( newLibraryPayload , {
include : Database . libraryFolderModel
} )
. catch ( ( error ) => {
Logger . error ( ` [LibraryController] Failed to create library " ${ newLibraryPayload . name } " ` , error )
} )
if ( ! library ) {
return res . status ( 500 ) . send ( 'Failed to create library' )
}
library . libraryFolders = await library . getLibraryFolders ( )
2022-12-01 00:32:59 +01:00
// Only emit to users with access to library
const userFilter = ( user ) => {
2024-08-24 22:38:15 +02:00
return user . checkCanAccessLibrary ? . ( library . id )
2022-12-01 00:32:59 +01:00
}
2024-08-24 22:38:15 +02:00
SocketAuthority . emitter ( 'library_added' , library . toOldJSON ( ) , userFilter )
2021-11-22 03:00:40 +01:00
// Add library watcher
2024-08-23 23:59:51 +02:00
this . watcher . addLibrary ( library )
2021-11-22 03:00:40 +01:00
2024-08-24 22:38:15 +02:00
res . json ( library . toOldJSON ( ) )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
/ * *
* GET : / a p i / l i b r a r i e s
* Get all libraries
*
* @ param { RequestWithUser } req
* @ param { Response } res
* /
2023-07-22 21:25:20 +02:00
async findAll ( req , res ) {
2024-08-29 00:26:23 +02:00
const libraries = await Database . libraryModel . getAllWithFolders ( )
2023-07-22 21:25:20 +02:00
2024-08-11 23:07:29 +02:00
const librariesAccessible = req . user . permissions ? . librariesAccessible || [ ]
2023-07-05 01:14:44 +02:00
if ( librariesAccessible . length ) {
2022-12-20 00:46:32 +01:00
return res . json ( {
2024-08-29 00:26:23 +02:00
libraries : libraries . filter ( ( lib ) => librariesAccessible . includes ( lib . id ) ) . map ( ( lib ) => lib . toOldJSON ( ) )
2022-12-20 00:46:32 +01:00
} )
2022-01-16 18:17:09 +01:00
}
2022-11-29 18:30:25 +01:00
res . json ( {
2024-08-29 00:26:23 +02:00
libraries : libraries . map ( ( lib ) => lib . toOldJSON ( ) )
2022-11-29 18:30:25 +01:00
} )
2021-11-22 03:00:40 +01:00
}
2024-02-11 23:48:16 +01:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2024-02-11 23:48:16 +01:00
* /
2021-12-01 03:02:40 +01:00
async findOne ( req , res ) {
2022-11-27 21:34:27 +01:00
const includeArray = ( req . query . include || '' ) . split ( ',' )
if ( includeArray . includes ( 'filterdata' ) ) {
2023-09-04 00:51:58 +02:00
const filterdata = await libraryFilters . getFilterData ( req . library . mediaType , req . library . id )
2024-02-11 23:48:16 +01:00
const customMetadataProviders = await Database . customMetadataProviderModel . getForClientByMediaType ( req . library . mediaType )
2023-08-13 22:10:26 +02:00
2021-12-01 03:02:40 +01:00
return res . json ( {
2023-08-13 22:10:26 +02:00
filterdata ,
issues : filterdata . numIssues ,
2024-08-11 23:07:29 +02:00
numUserPlaylists : await Database . playlistModel . getNumPlaylistsForUserAndLibrary ( req . user . id , req . library . id ) ,
2024-02-11 23:48:16 +01:00
customMetadataProviders ,
2024-08-29 00:26:23 +02:00
library : req . library . toOldJSON ( )
2021-12-01 03:02:40 +01:00
} )
2021-11-22 03:00:40 +01:00
}
2024-08-29 00:26:23 +02:00
res . json ( req . library . toOldJSON ( ) )
2021-11-22 03:00:40 +01:00
}
2023-08-13 22:10:26 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / e p i s o d e - d o w n l o a d s
* Get podcast episodes in download queue
2024-08-24 22:38:15 +02:00
*
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-13 22:10:26 +02:00
* /
2023-03-05 17:35:34 +01:00
async getEpisodeDownloadQueue ( req , res ) {
const libraryDownloadQueueDetails = this . podcastManager . getDownloadQueueDetails ( req . library . id )
2023-08-13 22:10:26 +02:00
res . json ( libraryDownloadQueueDetails )
2023-02-27 03:56:07 +01:00
}
2024-03-31 21:57:55 +02:00
/ * *
* PATCH : / a p i / l i b r a r i e s / : i d
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ this { import ( '../routers/ApiRouter' ) }
*
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2024-03-31 21:57:55 +02:00
* /
2021-11-22 03:00:40 +01:00
async update ( req , res ) {
2024-08-24 22:38:15 +02:00
// Validation
const updatePayload = { }
const keysToCheck = [ 'name' , 'provider' , 'mediaType' , 'icon' ]
for ( const key of keysToCheck ) {
if ( ! req . body [ key ] ) continue
if ( typeof req . body [ key ] !== 'string' ) {
return res . status ( 400 ) . send ( ` Invalid request. ${ key } must be a string ` )
}
updatePayload [ key ] = req . body [ key ]
}
if ( req . body . displayOrder !== undefined ) {
if ( isNaN ( req . body . displayOrder ) ) {
return res . status ( 400 ) . send ( 'Invalid request. displayOrder must be a number' )
}
updatePayload . displayOrder = req . body . displayOrder
}
2022-02-03 23:39:05 +01:00
2024-02-11 23:48:16 +01:00
// Validate that the custom provider exists if given any
if ( req . body . provider ? . startsWith ( 'custom-' ) ) {
2024-05-15 00:24:39 +02:00
if ( ! ( await Database . customMetadataProviderModel . checkExistsBySlug ( req . body . provider ) ) ) {
2024-02-11 23:48:16 +01:00
Logger . error ( ` [LibraryController] Custom metadata provider " ${ req . body . provider } " does not exist ` )
return res . status ( 400 ) . send ( 'Custom metadata provider does not exist' )
}
}
2024-08-24 22:38:15 +02:00
// Validate settings
2024-10-25 00:19:51 +02:00
const defaultLibrarySettings = Database . libraryModel . getDefaultLibrarySettingsForMediaType ( req . library . mediaType )
2024-08-24 22:38:15 +02:00
const updatedSettings = {
2024-10-25 00:19:51 +02:00
... ( req . library . settings || defaultLibrarySettings )
2024-08-24 22:38:15 +02:00
}
let hasUpdates = false
let hasUpdatedDisableWatcher = false
let hasUpdatedScanCron = false
if ( req . body . settings ) {
for ( const key in req . body . settings ) {
2024-10-25 00:19:51 +02:00
if ( ! Object . keys ( defaultLibrarySettings ) . includes ( key ) ) {
continue
}
2024-08-24 22:38:15 +02:00
if ( key === 'metadataPrecedence' ) {
if ( ! Array . isArray ( req . body . settings [ key ] ) ) {
return res . status ( 400 ) . send ( 'Invalid request. Settings "metadataPrecedence" must be an array' )
}
if ( JSON . stringify ( req . body . settings [ key ] ) !== JSON . stringify ( updatedSettings [ key ] ) ) {
hasUpdates = true
updatedSettings [ key ] = [ ... req . body . settings [ key ] ]
Logger . debug ( ` [LibraryController] Library " ${ req . library . name } " updating setting " ${ key } " to " ${ updatedSettings [ key ] } " ` )
}
} else if ( key === 'autoScanCronExpression' || key === 'podcastSearchRegion' ) {
if ( req . body . settings [ key ] !== null && typeof req . body . settings [ key ] !== 'string' ) {
return res . status ( 400 ) . send ( ` Invalid request. Settings " ${ key } " must be a string ` )
}
if ( req . body . settings [ key ] !== updatedSettings [ key ] ) {
if ( key === 'autoScanCronExpression' ) hasUpdatedScanCron = true
hasUpdates = true
updatedSettings [ key ] = req . body . settings [ key ]
Logger . debug ( ` [LibraryController] Library " ${ req . library . name } " updating setting " ${ key } " to " ${ updatedSettings [ key ] } " ` )
}
2024-10-25 00:19:51 +02:00
} else if ( key === 'markAsFinishedPercentComplete' ) {
if ( req . body . settings [ key ] !== null && isNaN ( req . body . settings [ key ] ) ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be a number ` )
} else if ( req . body . settings [ key ] !== null && ( Number ( req . body . settings [ key ] ) < 0 || Number ( req . body . settings [ key ] ) > 100 ) ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be between 0 and 100 ` )
}
if ( req . body . settings [ key ] !== updatedSettings [ key ] ) {
hasUpdates = true
updatedSettings [ key ] = Number ( req . body . settings [ key ] )
Logger . debug ( ` [LibraryController] Library " ${ req . library . name } " updating setting " ${ key } " to " ${ updatedSettings [ key ] } " ` )
}
} else if ( key === 'markAsFinishedTimeRemaining' ) {
if ( req . body . settings [ key ] !== null && isNaN ( req . body . settings [ key ] ) ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be a number ` )
} else if ( req . body . settings [ key ] !== null && Number ( req . body . settings [ key ] ) < 0 ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be greater than or equal to 0 ` )
}
if ( req . body . settings [ key ] !== updatedSettings [ key ] ) {
hasUpdates = true
updatedSettings [ key ] = Number ( req . body . settings [ key ] )
Logger . debug ( ` [LibraryController] Library " ${ req . library . name } " updating setting " ${ key } " to " ${ updatedSettings [ key ] } " ` )
}
2024-08-24 22:38:15 +02:00
} else {
if ( typeof req . body . settings [ key ] !== typeof updatedSettings [ key ] ) {
return res . status ( 400 ) . send ( ` Invalid request. Setting " ${ key } " must be of type ${ typeof updatedSettings [ key ] } ` )
}
if ( req . body . settings [ key ] !== updatedSettings [ key ] ) {
if ( key === 'disableWatcher' ) hasUpdatedDisableWatcher = true
hasUpdates = true
updatedSettings [ key ] = req . body . settings [ key ]
Logger . debug ( ` [LibraryController] Library " ${ req . library . name } " updating setting " ${ key } " to " ${ updatedSettings [ key ] } " ` )
}
}
}
if ( hasUpdates ) {
updatePayload . settings = updatedSettings
req . library . changed ( 'settings' , true )
}
}
let hasFolderUpdates = false
2022-03-14 15:56:24 +01:00
// Validate new folder paths exist or can be created & resolve rel paths
// returns 400 if a new folder fails to access
2024-08-24 22:38:15 +02:00
if ( Array . isArray ( req . body . folders ) ) {
2022-12-01 00:32:59 +01:00
const newFolderPaths = [ ]
2024-05-15 00:24:39 +02:00
req . body . folders = req . body . folders . map ( ( f ) => {
2022-03-14 15:56:24 +01:00
if ( ! f . id ) {
2024-08-24 22:38:15 +02:00
const path = f . fullPath || f . path
f . path = fileUtils . filePathToPOSIX ( Path . resolve ( path ) )
newFolderPaths . push ( f . path )
2022-03-14 15:56:24 +01:00
}
return f
} )
2022-12-01 00:32:59 +01:00
for ( const path of newFolderPaths ) {
const pathExists = await fs . pathExists ( path )
2022-05-20 02:00:34 +02:00
if ( ! pathExists ) {
2024-05-15 00:24:39 +02:00
const success = await fs
. ensureDir ( path )
. then ( ( ) => true )
. catch ( ( error ) => {
Logger . error ( ` [LibraryController] Failed to ensure folder dir " ${ path } " ` , error )
return false
} )
2022-05-20 02:00:34 +02:00
if ( ! success ) {
return res . status ( 400 ) . send ( ` Invalid folder directory " ${ path } " ` )
}
2022-03-14 15:56:24 +01:00
}
2024-08-24 22:38:15 +02:00
// Create folder
const libraryFolder = await Database . libraryFolderModel . create ( {
path ,
libraryId : req . library . id
} )
Logger . info ( ` [LibraryController] Created folder " ${ libraryFolder . path } " for library " ${ req . library . name } " ` )
hasFolderUpdates = true
2022-03-14 15:56:24 +01:00
}
2023-08-14 00:45:53 +02:00
// Handle removing folders
2024-08-24 22:38:15 +02:00
for ( const folder of req . library . libraryFolders ) {
2024-05-15 00:24:39 +02:00
if ( ! req . body . folders . some ( ( f ) => f . id === folder . id ) ) {
2023-08-14 00:45:53 +02:00
// Remove library items in folder
2023-08-20 20:34:03 +02:00
const libraryItemsInFolder = await Database . libraryItemModel . findAll ( {
2023-08-14 00:45:53 +02:00
where : {
libraryFolderId : folder . id
} ,
attributes : [ 'id' , 'mediaId' , 'mediaType' ] ,
include : [
{
2023-08-20 20:34:03 +02:00
model : Database . podcastModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ] ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . podcastEpisodeModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ]
}
}
]
} )
2024-08-24 22:38:15 +02:00
Logger . info ( ` [LibraryController] Removed folder " ${ folder . path } " from library " ${ req . library . name } " with ${ libraryItemsInFolder . length } library items ` )
2023-08-14 00:45:53 +02:00
for ( const libraryItem of libraryItemsInFolder ) {
let mediaItemIds = [ ]
2024-08-24 22:38:15 +02:00
if ( req . library . isPodcast ) {
2024-05-15 00:24:39 +02:00
mediaItemIds = libraryItem . media . podcastEpisodes . map ( ( pe ) => pe . id )
2023-08-14 00:45:53 +02:00
} else {
mediaItemIds . push ( libraryItem . mediaId )
}
2024-08-24 22:38:15 +02:00
Logger . info ( ` [LibraryController] Removing library item " ${ libraryItem . id } " from folder " ${ folder . path } " ` )
2023-08-14 00:45:53 +02:00
await this . handleDeleteLibraryItem ( libraryItem . mediaType , libraryItem . id , mediaItemIds )
}
2024-08-24 22:38:15 +02:00
// Remove folder
await folder . destroy ( )
hasFolderUpdates = true
2023-08-14 00:45:53 +02:00
}
}
2022-03-14 15:56:24 +01:00
}
2024-08-24 22:38:15 +02:00
if ( Object . keys ( updatePayload ) . length ) {
req . library . set ( updatePayload )
if ( req . library . changed ( ) ) {
Logger . debug ( ` [LibraryController] Updated library " ${ req . library . name } " with changed keys ${ req . library . changed ( ) } ` )
hasUpdates = true
await req . library . save ( )
}
}
if ( hasUpdatedScanCron ) {
Logger . debug ( ` [LibraryController] Updated library " ${ req . library . name } " auto scan cron ` )
2022-08-18 01:44:21 +02:00
// Update auto scan cron
2024-08-24 22:38:15 +02:00
this . cronManager . updateLibraryScanCron ( req . library )
}
2022-08-18 01:44:21 +02:00
2024-08-24 22:38:15 +02:00
if ( hasFolderUpdates || hasUpdatedDisableWatcher ) {
req . library . libraryFolders = await req . library . getLibraryFolders ( )
2024-08-23 23:59:51 +02:00
// Update watcher
2024-08-24 22:38:15 +02:00
this . watcher . updateLibrary ( req . library )
2022-12-01 00:32:59 +01:00
2024-08-24 22:38:15 +02:00
hasUpdates = true
}
if ( hasUpdates ) {
2022-12-01 00:32:59 +01:00
// Only emit to users with access to library
const userFilter = ( user ) => {
2024-08-24 22:38:15 +02:00
return user . checkCanAccessLibrary ? . ( req . library . id )
2022-12-01 00:32:59 +01:00
}
2024-08-24 22:38:15 +02:00
SocketAuthority . emitter ( 'library_updated' , req . library . toOldJSON ( ) , userFilter )
2023-08-20 20:16:53 +02:00
2024-08-24 22:38:15 +02:00
await Database . resetLibraryIssuesFilterData ( req . library . id )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
return res . json ( req . library . toOldJSON ( ) )
2021-11-22 03:00:40 +01:00
}
2023-07-22 21:25:20 +02:00
/ * *
* DELETE : / a p i / l i b r a r i e s / : i d
* Delete a library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-07-22 21:25:20 +02:00
* /
2021-11-22 03:00:40 +01:00
async delete ( req , res ) {
// Remove library watcher
2024-08-23 23:59:51 +02:00
this . watcher . removeLibrary ( req . library )
2021-11-22 03:00:40 +01:00
2022-11-12 00:44:19 +01:00
// Remove collections for library
2024-08-24 22:38:15 +02:00
const numCollectionsRemoved = await Database . collectionModel . removeAllForLibrary ( req . library . id )
2023-07-22 23:18:55 +02:00
if ( numCollectionsRemoved ) {
2024-08-24 22:38:15 +02:00
Logger . info ( ` [Server] Removed ${ numCollectionsRemoved } collections for library " ${ req . library . name } " ` )
2022-11-12 00:44:19 +01:00
}
2022-03-13 00:45:32 +01:00
// Remove items in this library
2023-08-20 20:34:03 +02:00
const libraryItemsInLibrary = await Database . libraryItemModel . findAll ( {
2023-08-14 00:45:53 +02:00
where : {
2024-08-24 22:38:15 +02:00
libraryId : req . library . id
2023-08-14 00:45:53 +02:00
} ,
attributes : [ 'id' , 'mediaId' , 'mediaType' ] ,
include : [
{
2023-08-20 20:34:03 +02:00
model : Database . podcastModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ] ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . podcastEpisodeModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ]
}
}
]
} )
2024-08-24 22:38:15 +02:00
Logger . info ( ` [LibraryController] Removing ${ libraryItemsInLibrary . length } library items in library " ${ req . library . name } " ` )
2023-08-14 00:45:53 +02:00
for ( const libraryItem of libraryItemsInLibrary ) {
let mediaItemIds = [ ]
2024-08-24 22:38:15 +02:00
if ( req . library . isPodcast ) {
2024-05-15 00:24:39 +02:00
mediaItemIds = libraryItem . media . podcastEpisodes . map ( ( pe ) => pe . id )
2023-08-14 00:45:53 +02:00
} else {
mediaItemIds . push ( libraryItem . mediaId )
}
2024-08-24 22:38:15 +02:00
Logger . info ( ` [LibraryController] Removing library item " ${ libraryItem . id } " from library " ${ req . library . name } " ` )
2023-08-14 00:45:53 +02:00
await this . handleDeleteLibraryItem ( libraryItem . mediaType , libraryItem . id , mediaItemIds )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
const libraryJson = req . library . toOldJSON ( )
await Database . removeLibrary ( req . library . id )
2023-07-22 21:25:20 +02:00
// Re-order libraries
2023-08-20 20:34:03 +02:00
await Database . libraryModel . resetDisplayOrder ( )
2023-07-22 21:25:20 +02:00
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'library_removed' , libraryJson )
2023-08-20 20:16:53 +02:00
// Remove library filter data
2024-08-24 22:38:15 +02:00
if ( Database . libraryFilterData [ req . library . id ] ) {
delete Database . libraryFilterData [ req . library . id ]
2023-08-20 20:16:53 +02:00
}
2021-11-22 03:00:40 +01:00
return res . json ( libraryJson )
}
2023-09-08 20:42:19 +02:00
/ * *
* GET / api / libraries / : id / items
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-09-08 20:42:19 +02:00
* /
2023-09-04 22:26:07 +02:00
async getLibraryItems ( req , res ) {
2024-05-15 00:24:39 +02:00
const include = ( req . query . include || '' )
. split ( ',' )
. map ( ( v ) => v . trim ( ) . toLowerCase ( ) )
. filter ( ( v ) => ! ! v )
2023-07-29 01:03:31 +02:00
const payload = {
results : [ ] ,
total : undefined ,
2024-10-12 00:15:16 +02:00
limit : req . query . limit || 0 ,
page : req . query . page || 0 ,
2023-07-29 01:03:31 +02:00
sortBy : req . query . sort ,
sortDesc : req . query . desc === '1' ,
filterBy : req . query . filter ,
mediaType : req . library . mediaType ,
minified : req . query . minified === '1' ,
collapseseries : req . query . collapseseries === '1' ,
include : include . join ( ',' )
}
2024-09-26 23:48:38 +02:00
2023-07-29 01:03:31 +02:00
payload . offset = payload . page * payload . limit
2023-09-04 22:26:07 +02:00
// TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
2024-05-15 00:24:39 +02:00
const filterByGroup = payload . filterBy ? . split ( '.' ) . shift ( )
const filterByValue = filterByGroup ? libraryFilters . decode ( payload . filterBy . replace ( ` ${ filterByGroup } . ` , '' ) ) : null
if ( filterByGroup === 'series' && filterByValue !== 'no-series' && payload . collapseseries ) {
2023-09-04 22:26:07 +02:00
const seriesId = libraryFilters . decode ( payload . filterBy . split ( '.' ) [ 1 ] )
2024-08-24 23:09:54 +02:00
payload . results = await libraryHelpers . handleCollapseSubseries ( payload , seriesId , req . user , req . library )
2023-09-04 22:26:07 +02:00
} else {
2024-08-24 23:09:54 +02:00
const { libraryItems , count } = await Database . libraryItemModel . getByFilterAndSort ( req . library , req . user , payload )
2023-09-04 22:26:07 +02:00
payload . results = libraryItems
payload . total = count
2022-11-13 20:25:20 +01:00
}
2022-03-11 01:45:02 +01:00
res . json ( payload )
2021-11-22 03:00:40 +01:00
}
2023-08-14 00:45:53 +02:00
/ * *
2024-08-24 22:38:15 +02:00
* DELETE : / a p i / l i b r a r i e s / : i d / i s s u e s
2023-08-14 00:45:53 +02:00
* Remove all library items missing or invalid
2024-08-24 22:38:15 +02:00
*
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-14 00:45:53 +02:00
* /
2022-04-25 01:25:33 +02:00
async removeLibraryItemsWithIssues ( req , res ) {
2023-08-20 20:34:03 +02:00
const libraryItemsWithIssues = await Database . libraryItemModel . findAll ( {
2023-08-14 00:45:53 +02:00
where : {
2023-08-20 20:16:53 +02:00
libraryId : req . library . id ,
2023-08-14 00:45:53 +02:00
[ Sequelize . Op . or ] : [
{
isMissing : true
} ,
{
isInvalid : true
}
]
} ,
attributes : [ 'id' , 'mediaId' , 'mediaType' ] ,
include : [
{
2023-08-20 20:34:03 +02:00
model : Database . podcastModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ] ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . podcastEpisodeModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' ]
}
}
]
} )
2022-04-25 01:25:33 +02:00
if ( ! libraryItemsWithIssues . length ) {
Logger . warn ( ` [LibraryController] No library items have issues ` )
return res . sendStatus ( 200 )
}
Logger . info ( ` [LibraryController] Removing ${ libraryItemsWithIssues . length } items with issues ` )
for ( const libraryItem of libraryItemsWithIssues ) {
2023-08-14 00:45:53 +02:00
let mediaItemIds = [ ]
2024-08-24 22:38:15 +02:00
if ( req . library . isPodcast ) {
2024-05-15 00:24:39 +02:00
mediaItemIds = libraryItem . media . podcastEpisodes . map ( ( pe ) => pe . id )
2023-08-14 00:45:53 +02:00
} else {
mediaItemIds . push ( libraryItem . mediaId )
}
Logger . info ( ` [LibraryController] Removing library item " ${ libraryItem . id } " with issue ` )
await this . handleDeleteLibraryItem ( libraryItem . mediaType , libraryItem . id , mediaItemIds )
2022-04-25 01:25:33 +02:00
}
2023-08-20 20:16:53 +02:00
// Set numIssues to 0 for library filter data
if ( Database . libraryFilterData [ req . library . id ] ) {
Database . libraryFilterData [ req . library . id ] . numIssues = 0
}
2022-04-25 01:25:33 +02:00
res . sendStatus ( 200 )
}
2023-08-19 00:08:34 +02:00
/ * *
2024-05-15 00:24:39 +02:00
* GET : / a p i / l i b r a r i e s / : i d / s e r i e s
* Optional query string : ` ?include=rssfeed ` that adds ` rssFeed ` to series if a feed is open
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2024-05-15 00:24:39 +02:00
* /
2023-08-20 00:12:24 +02:00
async getAllSeriesForLibrary ( req , res ) {
2024-05-15 00:24:39 +02:00
const include = ( req . query . include || '' )
. split ( ',' )
. map ( ( v ) => v . trim ( ) . toLowerCase ( ) )
. filter ( ( v ) => ! ! v )
2023-08-19 00:08:34 +02:00
const payload = {
results : [ ] ,
total : 0 ,
2024-10-12 00:15:16 +02:00
limit : req . query . limit || 0 ,
page : req . query . page || 0 ,
2023-08-19 00:08:34 +02:00
sortBy : req . query . sort ,
sortDesc : req . query . desc === '1' ,
filterBy : req . query . filter ,
minified : req . query . minified === '1' ,
include : include . join ( ',' )
}
const offset = payload . page * payload . limit
2024-08-24 23:09:54 +02:00
const { series , count } = await seriesFilters . getFilteredSeries ( req . library , req . user , payload . filterBy , payload . sortBy , payload . sortDesc , include , payload . limit , offset )
2023-08-19 00:08:34 +02:00
payload . total = count
payload . results = series
res . json ( payload )
}
2023-07-08 00:59:17 +02:00
/ * *
2023-08-13 22:10:26 +02:00
* GET : / a p i / l i b r a r i e s / : i d / s e r i e s / : s e r i e s I d
2023-07-08 00:59:17 +02:00
*
* Optional includes ( e . g . ` ?include=rssfeed,progress ` )
* rssfeed : adds ` rssFeed ` to series object if a feed is open
* progress : adds ` progress ` to series object with { libraryItemIds : Array < llid > , libraryItemIdsFinished : Array < llid > , isFinished : boolean }
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res - Series
2023-07-08 00:59:17 +02:00
* /
async getSeriesForLibrary ( req , res ) {
2024-05-15 00:24:39 +02:00
const include = ( req . query . include || '' )
. split ( ',' )
. map ( ( v ) => v . trim ( ) . toLowerCase ( ) )
. filter ( ( v ) => ! ! v )
2023-07-08 00:59:17 +02:00
2023-09-03 00:49:28 +02:00
const series = await Database . seriesModel . findByPk ( req . params . seriesId )
2023-07-08 00:59:17 +02:00
if ( ! series ) return res . sendStatus ( 404 )
2024-09-01 22:08:56 +02:00
const libraryItemsInSeries = await libraryItemsBookFilters . getLibraryItemsForSeries ( series , req . user )
2023-07-08 00:59:17 +02:00
2024-09-01 22:08:56 +02:00
const seriesJson = series . toOldJSON ( )
2023-07-08 00:59:17 +02:00
if ( include . includes ( 'progress' ) ) {
2024-08-11 23:07:29 +02:00
const libraryItemsFinished = libraryItemsInSeries . filter ( ( li ) => ! ! req . user . getMediaProgress ( li . media . id ) ? . isFinished )
2023-07-08 00:59:17 +02:00
seriesJson . progress = {
2024-05-15 00:24:39 +02:00
libraryItemIds : libraryItemsInSeries . map ( ( li ) => li . id ) ,
libraryItemIdsFinished : libraryItemsFinished . map ( ( li ) => li . id ) ,
2023-07-08 00:59:17 +02:00
isFinished : libraryItemsFinished . length >= libraryItemsInSeries . length
}
}
if ( include . includes ( 'rssfeed' ) ) {
2023-07-17 23:48:46 +02:00
const feedObj = await this . rssFeedManager . findFeedForEntityId ( seriesJson . id )
2023-07-08 00:59:17 +02:00
seriesJson . rssFeed = feedObj ? . toJSONMinified ( ) || null
}
res . json ( seriesJson )
}
2023-08-13 22:10:26 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / c o l l e c t i o n s
* Get all collections for library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-13 22:10:26 +02:00
* /
2021-12-01 03:02:40 +01:00
async getCollectionsForLibrary ( req , res ) {
2024-05-15 00:24:39 +02:00
const include = ( req . query . include || '' )
. split ( ',' )
. map ( ( v ) => v . trim ( ) . toLowerCase ( ) )
. filter ( ( v ) => ! ! v )
2022-12-31 17:33:38 +01:00
const payload = {
2021-12-01 03:02:40 +01:00
results : [ ] ,
total : 0 ,
2024-10-12 00:15:16 +02:00
limit : req . query . limit || 0 ,
page : req . query . page || 0 ,
2021-12-01 03:02:40 +01:00
sortBy : req . query . sort ,
sortDesc : req . query . desc === '1' ,
2021-12-24 23:37:57 +01:00
filterBy : req . query . filter ,
2022-12-31 17:33:38 +01:00
minified : req . query . minified === '1' ,
include : include . join ( ',' )
2021-12-01 03:02:40 +01:00
}
2023-08-12 00:49:06 +02:00
// TODO: Create paginated queries
2024-08-11 23:07:29 +02:00
let collections = await Database . collectionModel . getOldCollectionsJsonExpanded ( req . user , req . library . id , include )
2022-04-22 02:29:15 +02:00
2021-12-01 03:02:40 +01:00
payload . total = collections . length
if ( payload . limit ) {
2022-12-31 17:33:38 +01:00
const startIndex = payload . page * payload . limit
2021-12-01 03:02:40 +01:00
collections = collections . slice ( startIndex , startIndex + payload . limit )
}
payload . results = collections
res . json ( payload )
}
2023-08-13 22:10:26 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / p l a y l i s t s
* Get playlists for user in library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-13 22:10:26 +02:00
* /
2022-11-27 00:24:46 +01:00
async getUserPlaylistsForLibrary ( req , res ) {
2024-08-11 23:07:29 +02:00
let playlistsForUser = await Database . playlistModel . getOldPlaylistsForUserAndLibrary ( req . user . id , req . library . id )
2022-11-27 00:24:46 +01:00
const payload = {
results : [ ] ,
total : playlistsForUser . length ,
2024-10-12 00:15:16 +02:00
limit : req . query . limit || 0 ,
page : req . query . page || 0
2022-11-27 00:24:46 +01:00
}
if ( payload . limit ) {
const startIndex = payload . page * payload . limit
playlistsForUser = playlistsForUser . slice ( startIndex , startIndex + payload . limit )
}
payload . results = playlistsForUser
res . json ( payload )
}
2023-08-13 22:10:26 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / f i l t e r d a t a
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-13 22:10:26 +02:00
* /
2022-03-11 01:45:02 +01:00
async getLibraryFilterData ( req , res ) {
2023-09-04 00:51:58 +02:00
const filterData = await libraryFilters . getFilterData ( req . library . mediaType , req . library . id )
2023-08-13 22:10:26 +02:00
res . json ( filterData )
2022-03-11 01:45:02 +01:00
}
2023-08-03 01:29:28 +02:00
/ * *
2023-08-20 00:12:24 +02:00
* GET : / a p i / l i b r a r i e s / : i d / p e r s o n a l i z e d
* Home page shelves
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-03 01:29:28 +02:00
* /
async getUserPersonalizedShelves ( req , res ) {
2024-10-06 23:29:30 +02:00
const limitPerShelf = req . query . limit || 10
2024-05-15 00:24:39 +02:00
const include = ( req . query . include || '' )
. split ( ',' )
. map ( ( v ) => v . trim ( ) . toLowerCase ( ) )
. filter ( ( v ) => ! ! v )
2024-08-24 23:09:54 +02:00
const shelves = await Database . libraryItemModel . getPersonalizedShelves ( req . library , req . user , include , limitPerShelf )
2023-08-03 01:29:28 +02:00
res . json ( shelves )
}
2023-07-22 21:25:20 +02:00
/ * *
* POST : / a p i / l i b r a r i e s / o r d e r
* Change the display order of libraries
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ typedef LibraryReorderObj
* @ property { string } id
* @ property { number } newOrder
*
* @ typedef { Request < { } , { } , LibraryReorderObj [ ] , { } > & RequestUserObject } LibraryReorderRequest
*
* @ param { LibraryReorderRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-07-22 21:25:20 +02:00
* /
2021-11-22 03:00:40 +01:00
async reorder ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user " ${ req . user } " attempted to reorder libraries ` )
2022-02-15 23:36:22 +01:00
return res . sendStatus ( 403 )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
const libraries = await Database . libraryModel . getAllWithFolders ( )
2021-11-22 03:00:40 +01:00
2023-07-22 21:25:20 +02:00
const orderdata = req . body
2024-08-24 22:38:15 +02:00
if ( ! Array . isArray ( orderdata ) || orderdata . some ( ( o ) => typeof o ? . id !== 'string' || typeof o ? . newOrder !== 'number' ) ) {
return res . status ( 400 ) . send ( 'Invalid request. Request body must be an array of objects' )
}
2023-07-22 21:25:20 +02:00
let hasUpdates = false
2021-11-22 03:00:40 +01:00
for ( let i = 0 ; i < orderdata . length ; i ++ ) {
2024-05-15 00:24:39 +02:00
const library = libraries . find ( ( lib ) => lib . id === orderdata [ i ] . id )
2021-11-22 03:00:40 +01:00
if ( ! library ) {
2022-03-18 01:10:47 +01:00
Logger . error ( ` [LibraryController] Invalid library not found in reorder ${ orderdata [ i ] . id } ` )
2024-08-24 22:38:15 +02:00
return res . status ( 400 ) . send ( ` Library not found with id ${ orderdata [ i ] . id } ` )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
if ( library . displayOrder === orderdata [ i ] . newOrder ) continue
library . displayOrder = orderdata [ i ] . newOrder
await library . save ( )
hasUpdates = true
2021-11-22 03:00:40 +01:00
}
if ( hasUpdates ) {
2023-07-22 21:25:20 +02:00
libraries . sort ( ( a , b ) => a . displayOrder - b . displayOrder )
2022-03-27 16:45:28 +02:00
Logger . debug ( ` [LibraryController] Updated library display orders ` )
2021-11-22 03:00:40 +01:00
} else {
2022-03-27 16:45:28 +02:00
Logger . debug ( ` [LibraryController] Library orders were up to date ` )
2021-11-22 03:00:40 +01:00
}
2022-11-29 18:30:25 +01:00
res . json ( {
2024-08-24 22:38:15 +02:00
libraries : libraries . map ( ( lib ) => lib . toOldJSON ( ) )
2022-11-29 18:30:25 +01:00
} )
2021-11-22 03:00:40 +01:00
}
2023-08-19 20:59:22 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / s e a r c h
* Search library items with query
2024-08-12 00:01:25 +02:00
*
2023-08-19 20:59:22 +02:00
* ? q = search
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-19 20:59:22 +02:00
* /
async search ( req , res ) {
2023-12-17 19:23:55 +01:00
if ( ! req . query . q || typeof req . query . q !== 'string' ) {
return res . status ( 400 ) . send ( 'Invalid request. Query param "q" must be a string' )
2021-11-22 03:00:40 +01:00
}
2024-08-24 22:38:15 +02:00
2024-10-06 23:29:30 +02:00
const limit = req . query . limit || 12
2024-10-08 23:59:45 +02:00
const query = req . query . q . trim ( )
2023-08-19 20:59:22 +02:00
2024-08-24 22:38:15 +02:00
const matches = await libraryItemFilters . search ( req . user , req . library , query , limit )
2023-08-19 20:59:22 +02:00
res . json ( matches )
2021-12-02 02:07:03 +01:00
}
2023-08-19 23:53:33 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / s t a t s
* Get stats for library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-19 23:53:33 +02:00
* /
2021-12-02 02:07:03 +01:00
async stats ( req , res ) {
2023-08-19 23:53:33 +02:00
const stats = {
largestItems : await libraryItemFilters . getLargestItems ( req . library . id , 10 )
}
2024-08-23 23:59:51 +02:00
if ( req . library . mediaType === 'book' ) {
2024-05-01 13:20:48 +02:00
const authors = await authorFilters . getAuthorsWithCount ( req . library . id , 10 )
2023-08-19 23:53:33 +02:00
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 )
2024-04-30 17:09:06 +02:00
stats . totalAuthors = await authorFilters . getAuthorsTotalCount ( req . library . id )
2023-08-19 23:53:33 +02:00
stats . authorsWithCount = authors
stats . totalGenres = genres . length
stats . genresWithCount = genres
stats . totalItems = bookStats . totalItems
stats . longestItems = longestBooks
stats . totalSize = bookStats . totalSize
stats . totalDuration = bookStats . totalDuration
stats . numAudioTracks = bookStats . numAudioFiles
} else {
const genres = await libraryItemsPodcastFilters . getGenresWithCount ( req . library . id )
const podcastStats = await libraryItemsPodcastFilters . getPodcastLibraryStats ( req . library . id )
const longestPodcasts = await libraryItemsPodcastFilters . getLongestPodcasts ( req . library . id , 10 )
stats . totalGenres = genres . length
stats . genresWithCount = genres
stats . totalItems = podcastStats . totalItems
stats . longestItems = longestPodcasts
stats . totalSize = podcastStats . totalSize
stats . totalDuration = podcastStats . totalDuration
stats . numAudioTracks = podcastStats . numAudioFiles
2021-12-02 02:07:03 +01:00
}
res . json ( stats )
2021-11-22 03:00:40 +01:00
}
2021-12-01 03:02:40 +01:00
2023-08-14 00:45:53 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / a u t h o r s
* Get authors for library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-14 00:45:53 +02:00
* /
2021-12-03 02:02:38 +01:00
async getAuthors ( req , res ) {
2024-10-06 23:29:30 +02:00
const isPaginated = req . query . limit && ! isNaN ( req . query . limit ) && ! isNaN ( req . query . page )
2024-10-06 17:25:08 +02:00
const payload = {
results : [ ] ,
total : 0 ,
limit : isPaginated ? Number ( req . query . limit ) : 0 ,
page : isPaginated ? Number ( req . query . page ) : 0 ,
sortBy : req . query . sort ,
sortDesc : req . query . desc === '1' ,
filterBy : req . query . filter ,
minified : req . query . minified === '1' ,
include : req . query . include
}
// create order, limit and offset for pagination
let offset = isPaginated ? payload . page * payload . limit : undefined
let limit = isPaginated ? payload . limit : undefined
let order = undefined
const direction = payload . sortDesc ? 'DESC' : 'ASC'
if ( payload . sortBy === 'name' ) {
order = [ [ Sequelize . literal ( 'name COLLATE NOCASE' ) , direction ] ]
} else if ( payload . sortBy === 'lastFirst' ) {
order = [ [ Sequelize . literal ( 'lastFirst COLLATE NOCASE' ) , direction ] ]
} else if ( payload . sortBy === 'addedAt' ) {
order = [ [ 'createdAt' , direction ] ]
} else if ( payload . sortBy === 'updatedAt' ) {
order = [ [ 'updatedAt' , direction ] ]
} else if ( payload . sortBy === 'numBooks' ) {
offset = undefined
limit = undefined
}
2024-08-11 23:07:29 +02:00
const { bookWhere , replacements } = libraryItemsBookFilters . getUserPermissionBookWhereQuery ( req . user )
2024-10-06 17:25:08 +02:00
const { rows : authors , count } = await Database . authorModel . findAndCountAll ( {
2023-08-14 00:45:53 +02:00
where : {
libraryId : req . library . id
} ,
replacements ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . bookModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' , 'tags' , 'explicit' ] ,
where : bookWhere ,
2024-08-11 23:07:29 +02:00
required : ! req . user . isAdminOrUp , // Only show authors with 0 books for admin users or up
2023-08-14 00:45:53 +02:00
through : {
attributes : [ ]
}
} ,
2024-10-06 17:25:08 +02:00
order : order ,
limit : limit ,
offset : offset ,
distinct : true
2021-12-03 02:02:38 +01:00
} )
2022-04-25 00:15:41 +02:00
2024-10-06 17:25:08 +02:00
let oldAuthors = [ ]
2023-08-14 00:45:53 +02:00
for ( const author of authors ) {
2024-08-31 20:27:48 +02:00
const oldAuthor = author . toOldJSONExpanded ( author . books . length )
2024-02-04 04:48:35 +01:00
oldAuthor . lastFirst = author . lastFirst
2023-08-14 00:45:53 +02:00
oldAuthors . push ( oldAuthor )
}
2024-10-06 17:25:08 +02:00
// numBooks sort is handled post-query
if ( payload . sortBy === 'numBooks' ) {
oldAuthors . sort ( ( a , b ) => ( payload . sortDesc ? b . numBooks - a . numBooks : a . numBooks - b . numBooks ) )
if ( isPaginated ) {
const startIndex = payload . page * payload . limit
const endIndex = startIndex + payload . limit
oldAuthors = oldAuthors . slice ( startIndex , endIndex )
}
}
payload . results = oldAuthors
if ( isPaginated ) {
payload . total = count
res . json ( payload )
} else {
res . json ( {
authors : payload . results
} )
}
2021-12-03 02:02:38 +01:00
}
2023-08-14 00:45:53 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / n a r r a t o r s
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-14 00:45:53 +02:00
* /
2023-04-30 21:11:54 +02:00
async getNarrators ( req , res ) {
2023-08-14 00:45:53 +02:00
// Get all books with narrators
2023-08-20 20:34:03 +02:00
const booksWithNarrators = await Database . bookModel . findAll ( {
2023-08-14 00:45:53 +02:00
where : Sequelize . where ( Sequelize . fn ( 'json_array_length' , Sequelize . col ( 'narrators' ) ) , {
[ Sequelize . Op . gt ] : 0
} ) ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . libraryItemModel ,
2023-08-14 00:45:53 +02:00
attributes : [ 'id' , 'libraryId' ] ,
where : {
libraryId : req . library . id
}
} ,
attributes : [ 'id' , 'narrators' ]
} )
2023-04-30 21:11:54 +02:00
const narrators = { }
2023-08-14 00:45:53 +02:00
for ( const book of booksWithNarrators ) {
2024-05-15 00:24:39 +02:00
book . narrators . forEach ( ( n ) => {
2023-08-14 00:45:53 +02:00
if ( typeof n !== 'string' ) {
Logger . error ( ` [LibraryController] getNarrators: Invalid narrator " ${ n } " on book " ${ book . title } " ` )
} else if ( ! narrators [ n ] ) {
narrators [ n ] = {
id : encodeURIComponent ( Buffer . from ( n ) . toString ( 'base64' ) ) ,
name : n ,
numBooks : 1
2023-04-30 21:11:54 +02:00
}
2023-08-14 00:45:53 +02:00
} else {
narrators [ n ] . numBooks ++
}
} )
}
2023-04-30 21:11:54 +02:00
res . json ( {
2024-05-15 00:24:39 +02:00
narrators : naturalSort ( Object . values ( narrators ) ) . asc ( ( n ) => n . name )
2023-04-30 21:11:54 +02:00
} )
}
2023-08-14 00:45:53 +02:00
/ * *
* PATCH : / a p i / l i b r a r i e s / : i d / n a r r a t o r s / : n a r r a t o r I d
* Update narrator name
* : narratorId is base64 encoded name
* req . body { name }
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-14 00:45:53 +02:00
* /
2023-04-30 21:11:54 +02:00
async updateNarrator ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . canUpdate ) {
Logger . error ( ` [LibraryController] Unauthorized user " ${ req . user . username } " attempted to update narrator ` )
2023-04-30 21:11:54 +02:00
return res . sendStatus ( 403 )
}
2023-09-04 22:26:07 +02:00
const narratorName = libraryFilters . decode ( req . params . narratorId )
2023-04-30 21:11:54 +02:00
const updatedName = req . body . name
if ( ! updatedName ) {
return res . status ( 400 ) . send ( 'Invalid request payload. Name not specified.' )
}
2023-08-14 00:45:53 +02:00
// Update filter data
2023-09-02 01:01:17 +02:00
Database . replaceNarratorInFilterData ( narratorName , updatedName )
2023-08-14 00:45:53 +02:00
2023-04-30 21:11:54 +02:00
const itemsUpdated = [ ]
2023-08-14 00:45:53 +02:00
const itemsWithNarrator = await libraryItemFilters . getAllLibraryItemsWithNarrators ( [ narratorName ] )
for ( const libraryItem of itemsWithNarrator ) {
2024-05-15 00:24:39 +02:00
libraryItem . media . narrators = libraryItem . media . narrators . filter ( ( n ) => n !== narratorName )
2023-08-14 00:45:53 +02:00
if ( ! libraryItem . media . narrators . includes ( updatedName ) ) {
libraryItem . media . narrators . push ( updatedName )
2023-04-30 21:11:54 +02:00
}
2023-08-14 00:45:53 +02:00
await libraryItem . media . update ( {
narrators : libraryItem . media . narrators
} )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-14 00:45:53 +02:00
itemsUpdated . push ( oldLibraryItem )
2023-04-30 21:11:54 +02:00
}
if ( itemsUpdated . length ) {
2024-05-15 00:24:39 +02:00
SocketAuthority . emitter (
'items_updated' ,
itemsUpdated . map ( ( li ) => li . toJSONExpanded ( ) )
)
2023-04-30 21:11:54 +02:00
}
res . json ( {
updated : itemsUpdated . length
} )
}
2023-08-14 00:45:53 +02:00
/ * *
* DELETE : / a p i / l i b r a r i e s / : i d / n a r r a t o r s / : n a r r a t o r I d
* Remove narrator
* : narratorId is base64 encoded name
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-14 00:45:53 +02:00
* /
2023-04-30 21:11:54 +02:00
async removeNarrator ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . canUpdate ) {
Logger . error ( ` [LibraryController] Unauthorized user " ${ req . user . username } " attempted to remove narrator ` )
2023-04-30 21:11:54 +02:00
return res . sendStatus ( 403 )
}
2023-09-04 22:26:07 +02:00
const narratorName = libraryFilters . decode ( req . params . narratorId )
2023-04-30 21:11:54 +02:00
2023-08-14 00:45:53 +02:00
// Update filter data
Database . removeNarratorFromFilterData ( narratorName )
2023-04-30 21:11:54 +02:00
const itemsUpdated = [ ]
2023-08-14 00:45:53 +02:00
const itemsWithNarrator = await libraryItemFilters . getAllLibraryItemsWithNarrators ( [ narratorName ] )
for ( const libraryItem of itemsWithNarrator ) {
2024-05-15 00:24:39 +02:00
libraryItem . media . narrators = libraryItem . media . narrators . filter ( ( n ) => n !== narratorName )
2023-08-14 00:45:53 +02:00
await libraryItem . media . update ( {
narrators : libraryItem . media . narrators
} )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-14 00:45:53 +02:00
itemsUpdated . push ( oldLibraryItem )
2023-04-30 21:11:54 +02:00
}
if ( itemsUpdated . length ) {
2024-05-15 00:24:39 +02:00
SocketAuthority . emitter (
'items_updated' ,
itemsUpdated . map ( ( li ) => li . toJSONExpanded ( ) )
)
2023-04-30 21:11:54 +02:00
}
res . json ( {
updated : itemsUpdated . length
} )
}
2023-10-21 20:53:00 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / m a t c h a l l
* Quick match all library items . Book libraries only .
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-10-21 20:53:00 +02:00
* /
2022-04-21 01:05:09 +02:00
async matchAll ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-root user " ${ req . user . username } " attempted to match library items ` )
2022-02-15 23:36:22 +01:00
return res . sendStatus ( 403 )
}
2024-08-29 00:26:23 +02:00
Scanner . matchLibraryItems ( req . library )
2022-02-15 23:36:22 +01:00
res . sendStatus ( 200 )
}
2023-10-09 00:10:43 +02:00
/ * *
* POST : / a p i / l i b r a r i e s / : i d / s c a n
* Optional query :
* ? force = 1
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-10-09 00:10:43 +02:00
* /
2022-03-18 17:51:55 +01:00
async scan ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user " ${ req . user . username } " attempted to scan library ` )
2022-03-18 17:51:55 +01:00
return res . sendStatus ( 403 )
}
res . sendStatus ( 200 )
2024-08-29 00:26:23 +02:00
2023-10-09 00:10:43 +02:00
const forceRescan = req . query . force === '1'
2024-08-29 00:26:23 +02:00
await LibraryScanner . scan ( req . library , forceRescan )
2023-09-04 00:51:58 +02:00
2023-08-20 20:16:53 +02:00
await Database . resetLibraryIssuesFilterData ( req . library . id )
2022-03-18 17:51:55 +01:00
Logger . info ( '[LibraryController] Scan complete' )
}
2023-08-19 21:49:06 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / r e c e n t - e p i s o d e s
* Used for latest page
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-19 21:49:06 +02:00
* /
2022-09-16 23:59:16 +02:00
async getRecentEpisodes ( req , res ) {
2024-08-23 23:59:51 +02:00
if ( req . library . mediaType !== 'podcast' ) {
2022-09-16 23:59:16 +02:00
return res . sendStatus ( 404 )
}
2024-08-24 23:09:54 +02:00
2022-09-16 23:59:16 +02:00
const payload = {
episodes : [ ] ,
2024-10-12 00:15:16 +02:00
limit : req . query . limit || 0 ,
page : req . query . page || 0
2022-09-16 23:59:16 +02:00
}
2023-08-19 21:49:06 +02:00
const offset = payload . page * payload . limit
2024-08-24 23:09:54 +02:00
payload . episodes = await libraryItemsPodcastFilters . getRecentEpisodes ( req . user , req . library , payload . limit , offset )
2022-09-16 23:59:16 +02:00
res . json ( payload )
}
2023-08-19 22:19:27 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / o p m l
* Get OPML file for a podcast library
2024-08-12 00:01:25 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-08-19 22:19:27 +02:00
* /
async getOPMLFile ( req , res ) {
2024-08-11 23:07:29 +02:00
const userPermissionPodcastWhere = libraryItemsPodcastFilters . getUserPermissionPodcastWhereQuery ( req . user )
2023-08-19 22:19:27 +02:00
const podcasts = await Database . podcastModel . findAll ( {
attributes : [ 'id' , 'feedURL' , 'title' , 'description' , 'itunesPageURL' , 'language' ] ,
where : userPermissionPodcastWhere . podcastWhere ,
replacements : userPermissionPodcastWhere . replacements ,
include : {
model : Database . libraryItemModel ,
attributes : [ 'id' , 'libraryId' ] ,
where : {
libraryId : req . library . id
}
}
} )
const opmlText = this . podcastManager . generateOPMLFileText ( podcasts )
2023-05-28 22:10:34 +02:00
res . type ( 'application/xml' )
res . send ( opmlText )
}
2023-10-18 00:46:43 +02:00
/ * *
2024-08-24 22:38:15 +02:00
* POST : / a p i / l i b r a r i e s / : i d / r e m o v e - m e t a d a t a
2023-10-18 00:46:43 +02:00
* Remove all metadata . json or metadata . abs files in library item folders
2024-05-15 00:24:39 +02:00
*
2024-08-24 22:38:15 +02:00
* @ param { LibraryControllerRequest } req
2024-08-12 00:01:25 +02:00
* @ param { Response } res
2023-10-18 00:46:43 +02:00
* /
async removeAllMetadataFiles ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user " ${ req . user . username } " attempted to remove all metadata files ` )
2023-10-18 00:46:43 +02:00
return res . sendStatus ( 403 )
}
const fileExt = req . query . ext === 'abs' ? 'abs' : 'json'
const metadataFilename = ` metadata. ${ fileExt } `
const libraryItemsWithMetadata = await Database . libraryItemModel . findAll ( {
attributes : [ 'id' , 'libraryFiles' ] ,
where : [
{
libraryId : req . library . id
} ,
Sequelize . where ( Sequelize . literal ( ` (SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(libraryFiles) AND json_extract(json_each.value, " $ .metadata.filename") = " ${ metadataFilename } ") ` ) , {
[ Sequelize . Op . gte ] : 1
} )
]
} )
if ( ! libraryItemsWithMetadata . length ) {
Logger . info ( ` [LibraryController] No ${ metadataFilename } files found to remove ` )
return res . json ( {
found : 0
} )
}
Logger . info ( ` [LibraryController] Found ${ libraryItemsWithMetadata . length } ${ metadataFilename } files to remove ` )
let numRemoved = 0
for ( const libraryItem of libraryItemsWithMetadata ) {
2024-05-15 00:24:39 +02:00
const metadataFilepath = libraryItem . libraryFiles . find ( ( lf ) => lf . metadata . filename === metadataFilename ) ? . metadata . path
2023-10-18 00:46:43 +02:00
if ( ! metadataFilepath ) continue
Logger . debug ( ` [LibraryController] Removing file " ${ metadataFilepath } " ` )
2024-05-15 00:24:39 +02:00
if ( await fileUtils . removeFile ( metadataFilepath ) ) {
2023-10-18 00:46:43 +02:00
numRemoved ++
}
}
res . json ( {
found : libraryItemsWithMetadata . length ,
removed : numRemoved
} )
}
2024-10-11 23:55:09 +02:00
/ * *
* GET : / a p i / l i b r a r i e s / : i d / p o d c a s t - t i t l e s
*
* Get podcast titles with itunesId and libraryItemId for library
* Used on the podcast add page in order to check if a podcast is already in the library and redirect to it
*
* @ param { LibraryControllerRequest } req
* @ param { Response } res
* /
async getPodcastTitles ( req , res ) {
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user " ${ req . user . username } " attempted to get podcast titles ` )
return res . sendStatus ( 403 )
}
const podcasts = await Database . podcastModel . findAll ( {
attributes : [ 'id' , 'title' , 'itunesId' ] ,
include : {
model : Database . libraryItemModel ,
attributes : [ 'id' , 'libraryId' ] ,
where : {
libraryId : req . library . id
}
}
} )
res . json ( {
podcasts : podcasts . map ( ( p ) => {
return {
title : p . title ,
itunesId : p . itunesId ,
libraryItemId : p . libraryItem . id ,
libraryId : p . libraryItem . libraryId
}
} )
} )
}
2023-08-12 00:49:06 +02:00
/ * *
2024-08-12 00:01:25 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
* @ param { NextFunction } next
2023-08-12 00:49:06 +02:00
* /
2023-09-04 22:26:07 +02:00
async middleware ( req , res , next ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . checkCanAccessLibrary ( req . params . id ) ) {
Logger . warn ( ` [LibraryController] Library ${ req . params . id } not accessible to user ${ req . user . username } ` )
2023-08-12 00:49:06 +02:00
return res . sendStatus ( 403 )
}
2024-08-23 23:59:51 +02:00
const library = await Database . libraryModel . findByIdWithFolders ( req . params . id )
2023-08-12 00:49:06 +02:00
if ( ! library ) {
return res . status ( 404 ) . send ( 'Library not found' )
}
req . library = library
2024-10-06 23:29:30 +02:00
// Ensure pagination query params are positive integers
for ( const queryKey of [ 'limit' , 'page' ] ) {
if ( req . query [ queryKey ] !== undefined ) {
req . query [ queryKey ] = ! isNaN ( req . query [ queryKey ] ) ? Number ( req . query [ queryKey ] ) : 0
if ( ! Number . isInteger ( req . query [ queryKey ] ) || req . query [ queryKey ] < 0 ) {
return res . status ( 400 ) . send ( ` Invalid request. ${ queryKey } must be a positive integer ` )
}
}
}
2023-08-12 00:49:06 +02:00
next ( )
}
2021-11-22 03:00:40 +01:00
}
2023-02-19 22:39:28 +01:00
module . exports = new LibraryController ( )