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-11-22 03:00:40 +01:00
const Library = require ( '../objects/Library' )
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 { asciiOnlyToLowerCase } = require ( '../utils/index' )
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
2021-11-22 03:00:40 +01:00
class LibraryController {
constructor ( ) { }
async create ( req , res ) {
2022-12-01 00:32:59 +01:00
const newLibraryPayload = {
2021-11-22 03:00:40 +01:00
... req . body
}
if ( ! newLibraryPayload . name || ! newLibraryPayload . folders || ! newLibraryPayload . folders . length ) {
return res . status ( 500 ) . send ( 'Invalid request' )
}
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
newLibraryPayload . folders = newLibraryPayload . folders . map ( f => {
2023-09-04 00:51:58 +02:00
f . fullPath = fileUtils . filePathToPOSIX ( Path . resolve ( f . fullPath ) )
2022-03-19 12:41:54 +01:00
return f
} )
2022-12-01 00:32:59 +01:00
for ( const folder of newLibraryPayload . folders ) {
2022-04-21 00:49:34 +02:00
try {
2022-12-01 00:32:59 +01:00
const direxists = await fs . pathExists ( folder . fullPath )
2022-04-21 00:49:34 +02:00
if ( ! direxists ) { // If folder does not exist try to make it and set file permissions/owner
await fs . mkdir ( folder . fullPath )
}
} catch ( error ) {
2022-03-19 12:41:54 +01:00
Logger . error ( ` [LibraryController] Failed to ensure folder dir " ${ folder . fullPath } " ` , error )
return res . status ( 400 ) . send ( ` Invalid folder directory " ${ folder . fullPath } " ` )
}
}
2022-12-01 00:32:59 +01:00
const library = new Library ( )
2023-07-09 18:39:15 +02:00
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
2021-11-22 03:00:40 +01:00
library . setData ( newLibraryPayload )
2023-07-05 01:14:44 +02:00
await Database . createLibrary ( library )
2022-12-01 00:32:59 +01:00
// Only emit to users with access to library
const userFilter = ( user ) => {
2023-07-05 01:14:44 +02:00
return user . checkCanAccessLibrary ? . ( library . id )
2022-12-01 00:32:59 +01:00
}
SocketAuthority . emitter ( 'library_added' , library . toJSON ( ) , userFilter )
2021-11-22 03:00:40 +01:00
// Add library watcher
this . watcher . addLibrary ( library )
res . json ( library )
}
2023-07-22 21:25:20 +02:00
async findAll ( req , res ) {
2023-08-20 20:34:03 +02:00
const libraries = await Database . libraryModel . getAllOldLibraries ( )
2023-07-22 21:25:20 +02:00
2022-12-01 00:32:59 +01:00
const librariesAccessible = req . user . librariesAccessible || [ ]
2023-07-05 01:14:44 +02:00
if ( librariesAccessible . length ) {
2022-12-20 00:46:32 +01:00
return res . json ( {
2023-07-22 21:25:20 +02:00
libraries : libraries . filter ( lib => librariesAccessible . includes ( lib . id ) ) . map ( lib => lib . toJSON ( ) )
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 ( {
2023-07-22 21:25:20 +02:00
libraries : libraries . map ( lib => lib . toJSON ( ) )
2022-11-29 18:30:25 +01:00
} )
2021-11-22 03:00:40 +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 )
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 ,
2023-08-20 20:34:03 +02:00
numUserPlaylists : await Database . playlistModel . getNumPlaylistsForUserAndLibrary ( req . user . id , req . library . id ) ,
2021-12-01 03:02:40 +01:00
library : req . library
} )
2021-11-22 03:00:40 +01:00
}
2021-12-01 03:02:40 +01:00
return res . json ( req . library )
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
* @ param { * } req
* @ param { * } res
* /
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
}
2021-11-22 03:00:40 +01:00
async update ( req , res ) {
2022-12-01 00:32:59 +01:00
const library = req . library
2022-02-03 23:39:05 +01:00
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
if ( req . body . folders ) {
2022-12-01 00:32:59 +01:00
const newFolderPaths = [ ]
2022-03-14 15:56:24 +01:00
req . body . folders = req . body . folders . map ( f => {
if ( ! f . id ) {
2023-09-04 00:51:58 +02:00
f . fullPath = fileUtils . filePathToPOSIX ( Path . resolve ( f . fullPath ) )
2022-03-14 15:56:24 +01:00
newFolderPaths . push ( f . fullPath )
}
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 ) {
// Ensure dir will recursively create directories which might be preferred over mkdir
2022-12-01 00:32:59 +01:00
const success = await fs . ensureDir ( path ) . then ( ( ) => true ) . catch ( ( error ) => {
2022-05-20 02:00:34 +02:00
Logger . error ( ` [LibraryController] Failed to ensure folder dir " ${ path } " ` , error )
return false
} )
if ( ! success ) {
return res . status ( 400 ) . send ( ` Invalid folder directory " ${ path } " ` )
}
2022-03-14 15:56:24 +01:00
}
}
2023-08-14 00:45:53 +02:00
// Handle removing folders
for ( const folder of library . folders ) {
if ( ! req . body . folders . some ( f => f . id === folder . id ) ) {
// 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' ]
}
}
]
} )
Logger . info ( ` [LibraryController] Removed folder " ${ folder . fullPath } " from library " ${ library . name } " with ${ libraryItemsInFolder . length } library items ` )
for ( const libraryItem of libraryItemsInFolder ) {
let mediaItemIds = [ ]
if ( library . isPodcast ) {
mediaItemIds = libraryItem . media . podcastEpisodes . map ( pe => pe . id )
} else {
mediaItemIds . push ( libraryItem . mediaId )
}
Logger . info ( ` [LibraryController] Removing library item " ${ libraryItem . id } " from folder " ${ folder . fullPath } " ` )
await this . handleDeleteLibraryItem ( libraryItem . mediaType , libraryItem . id , mediaItemIds )
}
}
}
2022-03-14 15:56:24 +01:00
}
2022-12-01 00:32:59 +01:00
const hasUpdates = library . update ( req . body )
2022-02-03 23:39:05 +01:00
// TODO: Should check if this is an update to folder paths or name only
2021-11-22 03:00:40 +01:00
if ( hasUpdates ) {
// Update watcher
this . watcher . updateLibrary ( library )
2022-08-18 01:44:21 +02:00
// Update auto scan cron
this . cronManager . updateLibraryScanCron ( library )
2023-07-05 01:14:44 +02:00
await Database . updateLibrary ( library )
2022-12-01 00:32:59 +01:00
// Only emit to users with access to library
const userFilter = ( user ) => {
return user . checkCanAccessLibrary && user . checkCanAccessLibrary ( library . id )
}
SocketAuthority . emitter ( 'library_updated' , library . toJSON ( ) , userFilter )
2023-08-20 20:16:53 +02:00
await Database . resetLibraryIssuesFilterData ( library . id )
2021-11-22 03:00:40 +01:00
}
return res . json ( library . toJSON ( ) )
}
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
* @ param { * } req
* @ param { * } res
* /
2021-11-22 03:00:40 +01:00
async delete ( req , res ) {
2022-12-01 00:32:59 +01:00
const library = req . library
2021-11-22 03:00:40 +01:00
// Remove library watcher
this . watcher . removeLibrary ( library )
2022-11-12 00:44:19 +01:00
// Remove collections for library
2023-08-20 20:34:03 +02:00
const numCollectionsRemoved = await Database . collectionModel . removeAllForLibrary ( library . id )
2023-07-22 23:18:55 +02:00
if ( numCollectionsRemoved ) {
Logger . info ( ` [Server] Removed ${ numCollectionsRemoved } collections for library " ${ 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 : {
libraryId : library . 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' ]
}
}
]
} )
Logger . info ( ` [LibraryController] Removing ${ libraryItemsInLibrary . length } library items in library " ${ library . name } " ` )
for ( const libraryItem of libraryItemsInLibrary ) {
let mediaItemIds = [ ]
if ( library . isPodcast ) {
mediaItemIds = libraryItem . media . podcastEpisodes . map ( pe => pe . id )
} else {
mediaItemIds . push ( libraryItem . mediaId )
}
Logger . info ( ` [LibraryController] Removing library item " ${ libraryItem . id } " from library " ${ library . name } " ` )
await this . handleDeleteLibraryItem ( libraryItem . mediaType , libraryItem . id , mediaItemIds )
2021-11-22 03:00:40 +01:00
}
2022-12-01 00:32:59 +01:00
const libraryJson = library . toJSON ( )
2023-07-05 01:14:44 +02:00
await Database . removeLibrary ( 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
if ( Database . libraryFilterData [ library . id ] ) {
delete Database . libraryFilterData [ library . id ]
}
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
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2023-09-04 22:26:07 +02:00
async getLibraryItems ( req , res ) {
2023-07-29 01:03:31 +02:00
const include = ( req . query . include || '' ) . split ( ',' ) . map ( v => v . trim ( ) . toLowerCase ( ) ) . filter ( v => ! ! v )
const payload = {
results : [ ] ,
total : undefined ,
limit : req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 0 ,
page : req . query . page && ! isNaN ( req . query . page ) ? Number ( req . query . page ) : 0 ,
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 ( ',' )
}
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
if ( payload . filterBy ? . split ( '.' ) [ 0 ] === 'series' && payload . collapseseries ) {
const seriesId = libraryFilters . decode ( payload . filterBy . split ( '.' ) [ 1 ] )
payload . results = await libraryHelpers . handleCollapseSubseries ( payload , seriesId , req . user , req . library )
} else {
const { libraryItems , count } = await Database . libraryItemModel . getByFilterAndSort ( req . library , req . user , payload )
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
/ * *
* DELETE : /libraries/ : id / issues
* Remove all library items missing or invalid
2023-08-20 00:12:24 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . 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 = [ ]
2023-08-20 00:12:24 +02:00
if ( req . library . isPodcast ) {
2023-08-14 00:45:53 +02:00
mediaItemIds = libraryItem . media . podcastEpisodes . map ( pe => pe . id )
} 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
/ * *
2023-08-20 00:12:24 +02:00
* GET : / a p i / l i b r a r i e s / : i d / s e r i e s
2023-08-19 00:08:34 +02:00
* Optional query string : ` ?include=rssfeed ` that adds ` rssFeed ` to series if a feed is open
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2023-08-20 00:12:24 +02:00
async getAllSeriesForLibrary ( req , res ) {
2023-08-19 00:08:34 +02:00
const include = ( req . query . include || '' ) . split ( ',' ) . map ( v => v . trim ( ) . toLowerCase ( ) ) . filter ( v => ! ! v )
const payload = {
results : [ ] ,
total : 0 ,
limit : req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 0 ,
page : req . query . page && ! isNaN ( req . query . page ) ? Number ( req . query . page ) : 0 ,
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
const { series , count } = await seriesFilters . getFilteredSeries ( req . library , req . user , payload . filterBy , payload . sortBy , payload . sortDesc , include , payload . limit , offset )
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 }
*
2023-09-03 00:49:28 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res - Series
2023-07-08 00:59:17 +02:00
* /
async getSeriesForLibrary ( req , res ) {
const include = ( req . query . include || '' ) . split ( ',' ) . map ( v => v . trim ( ) . toLowerCase ( ) ) . filter ( v => ! ! v )
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 )
2023-09-03 00:49:28 +02:00
const oldSeries = series . getOldSeries ( )
2023-07-08 00:59:17 +02:00
2023-09-03 00:49:28 +02:00
const libraryItemsInSeries = await libraryItemsBookFilters . getLibraryItemsForSeries ( oldSeries , req . user )
2023-07-08 00:59:17 +02:00
2023-09-03 00:49:28 +02:00
const seriesJson = oldSeries . toJSON ( )
2023-07-08 00:59:17 +02:00
if ( include . includes ( 'progress' ) ) {
const libraryItemsFinished = libraryItemsInSeries . filter ( li => ! ! req . user . getMediaProgress ( li . id ) ? . isFinished )
seriesJson . progress = {
libraryItemIds : libraryItemsInSeries . map ( li => li . id ) ,
libraryItemIdsFinished : libraryItemsFinished . map ( li => li . id ) ,
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
* @ param { * } req
* @ param { * } res
* /
2021-12-01 03:02:40 +01:00
async getCollectionsForLibrary ( req , res ) {
2022-12-31 17:33:38 +01:00
const include = ( req . query . include || '' ) . split ( ',' ) . map ( v => v . trim ( ) . toLowerCase ( ) ) . filter ( v => ! ! v )
const payload = {
2021-12-01 03:02:40 +01:00
results : [ ] ,
total : 0 ,
limit : req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 0 ,
page : req . query . page && ! isNaN ( req . query . page ) ? Number ( req . query . page ) : 0 ,
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
2023-08-20 20:34:03 +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
* @ param { * } req
* @ param { * } res
* /
2022-11-27 00:24:46 +01:00
async getUserPlaylistsForLibrary ( req , res ) {
2023-08-20 20:34:03 +02:00
let playlistsForUser = await Database . playlistModel . getPlaylistsForUserAndLibrary ( req . user . id , req . library . id )
2023-08-13 22:10:26 +02:00
playlistsForUser = await Promise . all ( playlistsForUser . map ( async p => p . getOldJsonExpanded ( ) ) )
2022-11-27 00:24:46 +01:00
const payload = {
results : [ ] ,
total : playlistsForUser . length ,
limit : req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 0 ,
page : req . query . page && ! isNaN ( req . query . page ) ? Number ( req . query . page ) : 0
}
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
2023-08-20 00:12:24 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . 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
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
2023-08-03 01:29:28 +02:00
* /
async getUserPersonalizedShelves ( req , res ) {
const limitPerShelf = req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) || 10 : 10
const include = ( req . query . include || '' ) . split ( ',' ) . map ( v => v . trim ( ) . toLowerCase ( ) ) . filter ( v => ! ! v )
2023-08-20 00:12:24 +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
2023-08-20 00:12:24 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
2023-07-22 21:25:20 +02:00
* /
2021-11-22 03:00:40 +01:00
async reorder ( req , res ) {
2022-05-04 02:16:16 +02:00
if ( ! req . user . isAdminOrUp ) {
2022-03-18 01:10:47 +01:00
Logger . error ( '[LibraryController] ReorderLibraries invalid user' , req . user )
2022-02-15 23:36:22 +01:00
return res . sendStatus ( 403 )
2021-11-22 03:00:40 +01:00
}
2023-08-20 20:34:03 +02:00
const libraries = await Database . libraryModel . getAllOldLibraries ( )
2021-11-22 03:00:40 +01:00
2023-07-22 21:25:20 +02:00
const orderdata = req . body
let hasUpdates = false
2021-11-22 03:00:40 +01:00
for ( let i = 0 ; i < orderdata . length ; i ++ ) {
2023-07-22 21:25:20 +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 } ` )
2021-11-22 03:00:40 +01:00
return res . sendStatus ( 500 )
}
if ( library . update ( { displayOrder : orderdata [ i ] . newOrder } ) ) {
hasUpdates = true
2023-07-05 01:14:44 +02:00
await Database . updateLibrary ( library )
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 ( {
2023-07-22 21:25:20 +02:00
libraries : libraries . map ( lib => lib . toJSON ( ) )
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
* ? q = search
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async search ( req , res ) {
2021-11-22 03:00:40 +01:00
if ( ! req . query . q ) {
return res . status ( 400 ) . send ( 'No query string' )
}
2023-08-19 20:59:22 +02:00
const limit = req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 12
2023-10-06 00:00:40 +02:00
const query = asciiOnlyToLowerCase ( req . query . q . trim ( ) )
2023-08-19 20:59:22 +02:00
2023-08-19 21:11:34 +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
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
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 )
}
if ( req . library . isBook ) {
const authors = await authorFilters . getAuthorsWithCount ( req . library . id )
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 . 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
2023-08-18 21:40:36 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
2023-08-14 00:45:53 +02:00
* /
2021-12-03 02:02:38 +01:00
async getAuthors ( req , res ) {
2023-08-14 00:45:53 +02:00
const { bookWhere , replacements } = libraryItemsBookFilters . getUserPermissionBookWhereQuery ( req . user )
2023-08-18 21:40:36 +02:00
const authors = await Database . authorModel . findAll ( {
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 ,
2023-10-25 00:04:54 +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 : [ ]
}
} ,
order : [
[ Sequelize . literal ( 'name COLLATE NOCASE' ) , 'ASC' ]
]
2021-12-03 02:02:38 +01:00
} )
2022-04-25 00:15:41 +02:00
2023-08-14 00:45:53 +02:00
const oldAuthors = [ ]
for ( const author of authors ) {
const oldAuthor = author . getOldAuthor ( ) . toJSON ( )
oldAuthor . numBooks = author . books . length
oldAuthors . push ( oldAuthor )
}
2022-11-29 18:30:25 +01:00
res . json ( {
2023-08-14 00:45:53 +02:00
authors : oldAuthors
2022-11-29 18:30:25 +01:00
} )
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
* @ param { * } req
* @ param { * } res
* /
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 ) {
book . narrators . forEach ( n => {
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 ( {
narrators : naturalSort ( Object . values ( narrators ) ) . asc ( n => n . name )
} )
}
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 }
* @ param { * } req
* @ param { * } res
* /
2023-04-30 21:11:54 +02:00
async updateNarrator ( req , res ) {
if ( ! req . user . canUpdate ) {
Logger . error ( ` [LibraryController] Unauthorized user " ${ req . user . username } " attempted to update narrator ` )
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 ) {
libraryItem . media . narrators = libraryItem . media . narrators . filter ( n => n !== narratorName )
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 ) {
SocketAuthority . emitter ( 'items_updated' , itemsUpdated . map ( li => li . toJSONExpanded ( ) ) )
}
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
* @ param { * } req
* @ param { * } res
* /
2023-04-30 21:11:54 +02:00
async removeNarrator ( req , res ) {
if ( ! req . user . canUpdate ) {
Logger . error ( ` [LibraryController] Unauthorized user " ${ req . user . username } " attempted to remove narrator ` )
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 ) {
libraryItem . media . narrators = libraryItem . media . narrators . filter ( n => n !== narratorName )
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 ) {
SocketAuthority . emitter ( 'items_updated' , itemsUpdated . map ( li => li . toJSONExpanded ( ) ) )
}
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 .
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2022-04-21 01:05:09 +02:00
async matchAll ( req , res ) {
2022-05-04 02:16:16 +02:00
if ( ! req . user . isAdminOrUp ) {
2022-04-21 01:05:09 +02:00
Logger . error ( ` [LibraryController] Non-root user attempted to match library items ` , req . user )
2022-02-15 23:36:22 +01:00
return res . sendStatus ( 403 )
}
2023-09-07 00:48:50 +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
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2022-03-18 17:51:55 +01:00
async scan ( req , res ) {
2022-05-04 02:16:16 +02:00
if ( ! req . user . isAdminOrUp ) {
2022-03-18 17:51:55 +01:00
Logger . error ( ` [LibraryController] Non-root user attempted to scan library ` , req . user )
return res . sendStatus ( 403 )
}
res . sendStatus ( 200 )
2023-09-04 18:50:55 +02:00
2023-10-09 00:10:43 +02:00
const forceRescan = req . query . force === '1'
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
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2022-09-16 23:59:16 +02:00
async getRecentEpisodes ( req , res ) {
if ( ! req . library . isPodcast ) {
return res . sendStatus ( 404 )
}
const payload = {
episodes : [ ] ,
limit : req . query . limit && ! isNaN ( req . query . limit ) ? Number ( req . query . limit ) : 0 ,
page : req . query . page && ! isNaN ( req . query . page ) ? Number ( req . query . page ) : 0 ,
}
2023-08-19 21:49:06 +02:00
const offset = payload . page * payload . limit
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
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async getOPMLFile ( req , res ) {
const userPermissionPodcastWhere = libraryItemsPodcastFilters . getUserPermissionPodcastWhereQuery ( req . user )
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
/ * *
* Remove all metadata . json or metadata . abs files in library item folders
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async removeAllMetadataFiles ( req , res ) {
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [LibraryController] Non-admin user attempted to remove all metadata files ` , req . user )
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 ) {
const metadataFilepath = libraryItem . libraryFiles . find ( lf => lf . metadata . filename === metadataFilename ) ? . metadata . path
if ( ! metadataFilepath ) continue
Logger . debug ( ` [LibraryController] Removing file " ${ metadataFilepath } " ` )
if ( ( await fileUtils . removeFile ( metadataFilepath ) ) ) {
numRemoved ++
}
}
res . json ( {
found : libraryItemsWithMetadata . length ,
removed : numRemoved
} )
}
2023-08-12 00:49:06 +02:00
/ * *
* Middleware that is not using libraryItems from memory
2023-08-20 20:16:53 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* @ param { import ( 'express' ) . NextFunction } next
2023-08-12 00:49:06 +02:00
* /
2023-09-04 22:26:07 +02:00
async middleware ( req , res , next ) {
2023-08-12 00:49:06 +02:00
if ( ! req . user . checkCanAccessLibrary ( req . params . id ) ) {
Logger . warn ( ` [LibraryController] Library ${ req . params . id } not accessible to user ${ req . user . username } ` )
return res . sendStatus ( 403 )
}
2023-08-20 20:34:03 +02:00
const library = await Database . libraryModel . getOldById ( req . params . id )
2023-08-12 00:49:06 +02:00
if ( ! library ) {
return res . status ( 404 ) . send ( 'Library not found' )
}
req . library = library
next ( )
}
2021-11-22 03:00:40 +01:00
}
2023-02-19 22:39:28 +01:00
module . exports = new LibraryController ( )