2023-08-13 20:10:34 +02:00
const Sequelize = require ( 'sequelize' )
2022-03-18 17:51:55 +01:00
const Path = require ( 'path' )
2024-08-11 22:15:34 +02:00
const { Request , Response } = require ( 'express' )
2022-07-06 02:53:01 +02:00
const fs = require ( '../libs/fsExtra' )
2022-03-18 17:51:55 +01:00
const Logger = require ( '../Logger' )
2022-12-18 21:17:52 +01:00
const SocketAuthority = require ( '../SocketAuthority' )
2023-07-05 01:14:44 +02:00
const Database = require ( '../Database' )
2024-11-07 23:32:05 +01:00
const Watcher = require ( '../Watcher' )
2022-12-18 21:17:52 +01:00
2023-08-13 20:10:34 +02:00
const libraryItemFilters = require ( '../utils/queries/libraryItemFilters' )
2022-08-02 01:06:22 +02:00
const patternValidation = require ( '../libs/nodeCron/pattern-validation' )
2023-09-08 19:32:30 +02:00
const { isObject , getTitleIgnorePrefix } = require ( '../utils/index' )
2023-12-02 06:42:54 +01:00
const { sanitizeFilename } = require ( '../utils/fileUtils' )
2022-03-18 17:51:55 +01:00
2023-10-20 23:39:32 +02:00
const TaskManager = require ( '../managers/TaskManager' )
2023-12-23 00:01:07 +01:00
const adminStats = require ( '../utils/queries/adminStats' )
2023-10-20 23:39:32 +02:00
2024-08-11 22:15:34 +02:00
/ * *
2024-08-12 00:01:25 +02:00
* @ typedef RequestUserObject
2024-08-11 23:07:29 +02:00
* @ property { import ( '../models/User' ) } user
2024-08-11 22:15:34 +02:00
*
2024-08-12 00:01:25 +02:00
* @ typedef { Request & RequestUserObject } RequestWithUser
2024-08-11 22:15:34 +02:00
* /
2022-03-18 17:51:55 +01:00
class MiscController {
2024-08-10 22:46:04 +02:00
constructor ( ) { }
2022-03-18 17:51:55 +01:00
2023-08-13 20:10:34 +02:00
/ * *
* POST : / a p i / u p l o a d
* Update library item
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-03-18 17:51:55 +01:00
async handleUpload ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . canUpload ) {
Logger . warn ( ` User " ${ req . user . username } " attempted to upload without permission ` )
2022-03-18 17:51:55 +01:00
return res . sendStatus ( 403 )
}
2023-02-11 00:25:19 +01:00
if ( ! req . files ) {
Logger . error ( 'Invalid request, no files' )
return res . sendStatus ( 400 )
}
2023-12-02 06:42:54 +01:00
2023-07-22 21:25:20 +02:00
const files = Object . values ( req . files )
2023-12-02 06:42:54 +01:00
const { title , author , series , folder : folderId , library : libraryId } = req . body
2023-07-22 21:25:20 +02:00
2024-09-01 14:35:05 +02:00
const library = await Database . libraryModel . findByIdWithFolders ( libraryId )
2022-03-18 17:51:55 +01:00
if ( ! library ) {
2022-11-21 14:52:33 +01:00
return res . status ( 404 ) . send ( ` Library not found with id ${ libraryId } ` )
2022-03-18 17:51:55 +01:00
}
2024-08-24 23:09:54 +02:00
const folder = library . libraryFolders . find ( ( fold ) => fold . id === folderId )
2022-03-18 17:51:55 +01:00
if ( ! folder ) {
2022-11-21 14:52:33 +01:00
return res . status ( 404 ) . send ( ` Folder not found with id ${ folderId } in library ${ library . name } ` )
2022-03-18 17:51:55 +01:00
}
if ( ! files . length || ! title ) {
return res . status ( 500 ) . send ( ` Invalid post data ` )
}
2023-12-02 06:42:54 +01:00
// Podcasts should only be one folder deep
const outputDirectoryParts = library . isPodcast ? [ title ] : [ author , series , title ]
// `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`)
// before sanitizing all the directory parts to remove illegal chars and finally prepending
// the base folder path
2024-08-10 22:46:04 +02:00
const cleanedOutputDirectoryParts = outputDirectoryParts . filter ( Boolean ) . map ( ( part ) => sanitizeFilename ( part ) )
2024-08-24 23:09:54 +02:00
const outputDirectory = Path . join ( ... [ folder . path , ... cleanedOutputDirectoryParts ] )
2022-03-18 17:51:55 +01:00
await fs . ensureDir ( outputDirectory )
Logger . info ( ` Uploading ${ files . length } files to ` , outputDirectory )
2023-12-02 06:42:54 +01:00
for ( const file of files ) {
const path = Path . join ( outputDirectory , sanitizeFilename ( file . name ) )
2022-03-18 17:51:55 +01:00
2024-08-10 22:46:04 +02:00
await file
. mv ( path )
2023-12-02 06:42:54 +01:00
. then ( ( ) => {
return true
} )
. catch ( ( error ) => {
Logger . error ( 'Failed to move file' , path , error )
return false
} )
2022-03-18 17:51:55 +01:00
}
res . sendStatus ( 200 )
}
2023-08-13 20:10:34 +02:00
/ * *
* GET : / a p i / t a s k s
* Get tasks for task manager
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-10-02 21:16:17 +02:00
getTasks ( req , res ) {
2023-04-02 23:13:18 +02:00
const includeArray = ( req . query . include || '' ) . split ( ',' )
const data = {
2024-08-10 22:46:04 +02:00
tasks : TaskManager . tasks . map ( ( t ) => t . toJSON ( ) )
2023-04-02 23:13:18 +02:00
}
if ( includeArray . includes ( 'queue' ) ) {
data . queuedTaskData = {
embedMetadata : this . audioMetadataManager . getQueuedTaskData ( )
}
}
res . json ( data )
2022-03-18 17:51:55 +01:00
}
2023-08-13 20:10:34 +02:00
/ * *
* PATCH : / a p i / s e t t i n g s
* Update server settings
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-03-18 17:51:55 +01:00
async updateServerSettings ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` User " ${ req . user . username } " other than admin attempting to update server settings ` )
2022-03-18 17:51:55 +01:00
return res . sendStatus ( 403 )
}
2023-07-09 18:39:15 +02:00
const settingsUpdate = req . body
2023-11-10 23:11:51 +01:00
if ( ! isObject ( settingsUpdate ) ) {
2023-09-08 19:32:30 +02:00
return res . status ( 400 ) . send ( 'Invalid settings update object' )
2022-03-18 17:51:55 +01:00
}
2024-12-08 15:57:45 +01:00
if ( settingsUpdate . allowIframe == false && process . env . ALLOW _IFRAME === '1' ) {
Logger . warn ( 'Cannot disable iframe when ALLOW_IFRAME is enabled in environment' )
return res . status ( 400 ) . send ( 'Cannot disable iframe when ALLOW_IFRAME is enabled in environment' )
}
2022-03-18 17:51:55 +01:00
2023-07-09 18:39:15 +02:00
const madeUpdates = Database . serverSettings . update ( settingsUpdate )
2022-03-18 17:51:55 +01:00
if ( madeUpdates ) {
2023-07-09 18:39:15 +02:00
await Database . updateServerSettings ( )
2022-03-18 17:51:55 +01:00
// If backup schedule is updated - update backup manager
if ( settingsUpdate . backupSchedule !== undefined ) {
this . backupManager . updateCronSchedule ( )
}
}
return res . json ( {
2023-07-05 01:14:44 +02:00
serverSettings : Database . serverSettings . toJSONForBrowser ( )
2022-03-18 17:51:55 +01:00
} )
}
2023-09-08 19:32:30 +02:00
/ * *
* PATCH : / a p i / s o r t i n g - p r e f i x e s
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-09-08 19:32:30 +02:00
* /
async updateSortingPrefixes ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` User " ${ req . user . username } " other than admin attempting to update server sorting prefixes ` )
2023-09-08 19:32:30 +02:00
return res . sendStatus ( 403 )
}
let sortingPrefixes = req . body . sortingPrefixes
if ( ! sortingPrefixes ? . length || ! Array . isArray ( sortingPrefixes ) ) {
return res . status ( 400 ) . send ( 'Invalid request body' )
}
2024-08-10 22:46:04 +02:00
sortingPrefixes = [ ... new Set ( sortingPrefixes . map ( ( p ) => p ? . trim ? . ( ) . toLowerCase ( ) ) . filter ( ( p ) => p ) ) ]
2023-09-08 19:32:30 +02:00
if ( ! sortingPrefixes . length ) {
return res . status ( 400 ) . send ( 'Invalid sortingPrefixes in request body' )
}
Logger . debug ( ` [MiscController] Updating sorting prefixes ${ sortingPrefixes . join ( ', ' ) } ` )
Database . serverSettings . sortingPrefixes = sortingPrefixes
await Database . updateServerSettings ( )
let rowsUpdated = 0
// Update titleIgnorePrefix column on books
const books = await Database . bookModel . findAll ( {
attributes : [ 'id' , 'title' , 'titleIgnorePrefix' ]
} )
const bulkUpdateBooks = [ ]
books . forEach ( ( book ) => {
const titleIgnorePrefix = getTitleIgnorePrefix ( book . title )
if ( titleIgnorePrefix !== book . titleIgnorePrefix ) {
bulkUpdateBooks . push ( {
id : book . id ,
titleIgnorePrefix
} )
}
} )
if ( bulkUpdateBooks . length ) {
Logger . info ( ` [MiscController] Updating titleIgnorePrefix on ${ bulkUpdateBooks . length } books ` )
rowsUpdated += bulkUpdateBooks . length
await Database . bookModel . bulkCreate ( bulkUpdateBooks , {
updateOnDuplicate : [ 'titleIgnorePrefix' ]
} )
}
// Update titleIgnorePrefix column on podcasts
const podcasts = await Database . podcastModel . findAll ( {
attributes : [ 'id' , 'title' , 'titleIgnorePrefix' ]
} )
const bulkUpdatePodcasts = [ ]
podcasts . forEach ( ( podcast ) => {
const titleIgnorePrefix = getTitleIgnorePrefix ( podcast . title )
if ( titleIgnorePrefix !== podcast . titleIgnorePrefix ) {
bulkUpdatePodcasts . push ( {
id : podcast . id ,
titleIgnorePrefix
} )
}
} )
if ( bulkUpdatePodcasts . length ) {
Logger . info ( ` [MiscController] Updating titleIgnorePrefix on ${ bulkUpdatePodcasts . length } podcasts ` )
rowsUpdated += bulkUpdatePodcasts . length
await Database . podcastModel . bulkCreate ( bulkUpdatePodcasts , {
updateOnDuplicate : [ 'titleIgnorePrefix' ]
} )
}
// Update nameIgnorePrefix column on series
const allSeries = await Database . seriesModel . findAll ( {
attributes : [ 'id' , 'name' , 'nameIgnorePrefix' ]
} )
const bulkUpdateSeries = [ ]
allSeries . forEach ( ( series ) => {
const nameIgnorePrefix = getTitleIgnorePrefix ( series . name )
if ( nameIgnorePrefix !== series . nameIgnorePrefix ) {
bulkUpdateSeries . push ( {
id : series . id ,
nameIgnorePrefix
} )
}
} )
if ( bulkUpdateSeries . length ) {
Logger . info ( ` [MiscController] Updating nameIgnorePrefix on ${ bulkUpdateSeries . length } series ` )
rowsUpdated += bulkUpdateSeries . length
await Database . seriesModel . bulkCreate ( bulkUpdateSeries , {
updateOnDuplicate : [ 'nameIgnorePrefix' ]
} )
}
res . json ( {
rowsUpdated ,
serverSettings : Database . serverSettings . toJSONForBrowser ( )
} )
}
2023-07-22 21:25:20 +02:00
/ * *
* POST : / a p i / a u t h o r i z e
* Used to authorize an API token
2024-08-10 22:46:04 +02:00
*
* @ this import ( '../routers/ApiRouter' )
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-07-22 21:25:20 +02:00
* /
async authorize ( req , res ) {
2024-08-11 23:07:29 +02:00
const userResponse = await this . auth . getUserLoginResponsePayload ( req . user )
2022-04-30 00:43:46 +02:00
res . json ( userResponse )
2022-03-18 17:51:55 +01:00
}
2022-03-20 12:29:08 +01:00
2023-08-13 20:10:34 +02:00
/ * *
* GET : / a p i / t a g s
* Get all tags
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
async getAllTags ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to getAllTags ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-03-20 12:29:08 +01:00
}
2023-08-13 20:10:34 +02:00
2022-12-18 21:17:52 +01:00
const tags = [ ]
2023-08-20 20:34:03 +02:00
const books = await Database . bookModel . findAll ( {
2023-08-13 20:10:34 +02:00
attributes : [ 'tags' ] ,
where : Sequelize . where ( Sequelize . fn ( 'json_array_length' , Sequelize . col ( 'tags' ) ) , {
[ Sequelize . Op . gt ] : 0
} )
} )
for ( const book of books ) {
for ( const tag of book . tags ) {
if ( ! tags . includes ( tag ) ) tags . push ( tag )
2022-03-20 12:29:08 +01:00
}
2023-08-13 20:10:34 +02:00
}
2023-08-20 20:34:03 +02:00
const podcasts = await Database . podcastModel . findAll ( {
2023-08-13 20:10:34 +02:00
attributes : [ 'tags' ] ,
where : Sequelize . where ( Sequelize . fn ( 'json_array_length' , Sequelize . col ( 'tags' ) ) , {
[ Sequelize . Op . gt ] : 0
} )
2022-03-20 12:29:08 +01:00
} )
2023-08-13 20:10:34 +02:00
for ( const podcast of podcasts ) {
for ( const tag of podcast . tags ) {
if ( ! tags . includes ( tag ) ) tags . push ( tag )
}
}
2022-11-29 19:26:59 +01:00
res . json ( {
2024-04-10 00:54:09 +02:00
tags : tags . sort ( ( a , b ) => a . toLowerCase ( ) . localeCompare ( b . toLowerCase ( ) ) )
2022-11-29 19:26:59 +01:00
} )
2022-03-20 12:29:08 +01:00
}
2022-08-02 01:06:22 +02:00
2023-08-13 20:10:34 +02:00
/ * *
* POST : / a p i / t a g s / r e n a m e
* Rename tag
* Req . body { tag , newTag }
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-12-18 21:17:52 +01:00
async renameTag ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to renameTag ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-12-18 21:17:52 +01:00
}
const tag = req . body . tag
const newTag = req . body . newTag
if ( ! tag || ! newTag ) {
Logger . error ( ` [MiscController] Invalid request body for renameTag ` )
return res . sendStatus ( 400 )
}
let tagMerged = false
let numItemsUpdated = 0
2023-08-13 22:10:26 +02:00
// Update filter data
2023-09-02 01:01:17 +02:00
Database . replaceTagInFilterData ( tag , newTag )
2023-08-13 22:10:26 +02:00
2023-08-13 20:10:34 +02:00
const libraryItemsWithTag = await libraryItemFilters . getAllLibraryItemsWithTags ( [ tag , newTag ] )
for ( const libraryItem of libraryItemsWithTag ) {
2023-08-14 00:45:53 +02:00
if ( libraryItem . media . tags . includes ( newTag ) ) {
2023-08-13 20:10:34 +02:00
tagMerged = true // new tag is an existing tag so this is a merge
}
2022-12-18 21:17:52 +01:00
2023-08-14 00:45:53 +02:00
if ( libraryItem . media . tags . includes ( tag ) ) {
2024-08-10 22:46:04 +02:00
libraryItem . media . tags = libraryItem . media . tags . filter ( ( t ) => t !== tag ) // Remove old tag
2023-08-14 00:45:53 +02:00
if ( ! libraryItem . media . tags . includes ( newTag ) ) {
libraryItem . media . tags . push ( newTag )
2022-12-18 21:17:52 +01:00
}
2023-08-13 20:10:34 +02:00
Logger . debug ( ` [MiscController] Rename tag " ${ tag } " to " ${ newTag } " for item " ${ libraryItem . media . title } " ` )
await libraryItem . media . update ( {
2023-08-14 00:45:53 +02:00
tags : libraryItem . media . tags
2023-08-13 20:10:34 +02:00
} )
2024-04-13 00:34:10 +02:00
await libraryItem . saveMetadataFile ( )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-13 20:10:34 +02:00
SocketAuthority . emitter ( 'item_updated' , oldLibraryItem . toJSONExpanded ( ) )
2022-12-18 21:17:52 +01:00
numItemsUpdated ++
}
}
res . json ( {
tagMerged ,
numItemsUpdated
} )
}
2023-08-13 20:10:34 +02:00
/ * *
* DELETE : / a p i / t a g s / : t a g
* Remove a tag
* : tag param is base64 encoded
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-12-18 21:17:52 +01:00
async deleteTag ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to deleteTag ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-12-18 21:17:52 +01:00
}
const tag = Buffer . from ( decodeURIComponent ( req . params . tag ) , 'base64' ) . toString ( )
2023-08-13 20:10:34 +02:00
// Get all items with tag
const libraryItemsWithTag = await libraryItemFilters . getAllLibraryItemsWithTags ( [ tag ] )
2023-08-13 22:10:26 +02:00
// Update filterdata
Database . removeTagFromFilterData ( tag )
2022-12-18 21:17:52 +01:00
let numItemsUpdated = 0
2023-08-13 20:10:34 +02:00
// Remove tag from items
for ( const libraryItem of libraryItemsWithTag ) {
Logger . debug ( ` [MiscController] Remove tag " ${ tag } " from item " ${ libraryItem . media . title } " ` )
2024-08-10 22:46:04 +02:00
libraryItem . media . tags = libraryItem . media . tags . filter ( ( t ) => t !== tag )
2023-08-13 20:10:34 +02:00
await libraryItem . media . update ( {
2023-08-14 00:45:53 +02:00
tags : libraryItem . media . tags
2023-08-13 20:10:34 +02:00
} )
2024-04-13 00:34:10 +02:00
await libraryItem . saveMetadataFile ( )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-13 20:10:34 +02:00
SocketAuthority . emitter ( 'item_updated' , oldLibraryItem . toJSONExpanded ( ) )
numItemsUpdated ++
2022-12-18 21:17:52 +01:00
}
res . json ( {
numItemsUpdated
} )
}
2023-08-13 20:10:34 +02:00
/ * *
* GET : / a p i / g e n r e s
* Get all genres
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
async getAllGenres ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to getAllGenres ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-12-18 21:52:53 +01:00
}
const genres = [ ]
2023-08-20 20:34:03 +02:00
const books = await Database . bookModel . findAll ( {
2023-08-13 20:10:34 +02:00
attributes : [ 'genres' ] ,
where : Sequelize . where ( Sequelize . fn ( 'json_array_length' , Sequelize . col ( 'genres' ) ) , {
[ Sequelize . Op . gt ] : 0
} )
} )
for ( const book of books ) {
for ( const tag of book . genres ) {
if ( ! genres . includes ( tag ) ) genres . push ( tag )
2022-12-18 21:52:53 +01:00
}
2023-08-13 20:10:34 +02:00
}
2023-08-20 20:34:03 +02:00
const podcasts = await Database . podcastModel . findAll ( {
2023-08-13 20:10:34 +02:00
attributes : [ 'genres' ] ,
where : Sequelize . where ( Sequelize . fn ( 'json_array_length' , Sequelize . col ( 'genres' ) ) , {
[ Sequelize . Op . gt ] : 0
} )
2022-12-18 21:52:53 +01:00
} )
2023-08-13 20:10:34 +02:00
for ( const podcast of podcasts ) {
for ( const tag of podcast . genres ) {
if ( ! genres . includes ( tag ) ) genres . push ( tag )
}
}
2022-12-18 21:52:53 +01:00
res . json ( {
genres
} )
}
2023-08-13 20:10:34 +02:00
/ * *
* POST : / a p i / g e n r e s / r e n a m e
* Rename genres
* Req . body { genre , newGenre }
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-12-18 21:52:53 +01:00
async renameGenre ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to renameGenre ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-12-18 21:52:53 +01:00
}
const genre = req . body . genre
const newGenre = req . body . newGenre
if ( ! genre || ! newGenre ) {
Logger . error ( ` [MiscController] Invalid request body for renameGenre ` )
return res . sendStatus ( 400 )
}
let genreMerged = false
let numItemsUpdated = 0
2023-08-13 22:10:26 +02:00
// Update filter data
2023-09-02 01:01:17 +02:00
Database . replaceGenreInFilterData ( genre , newGenre )
2023-08-13 22:10:26 +02:00
2023-08-13 20:10:34 +02:00
const libraryItemsWithGenre = await libraryItemFilters . getAllLibraryItemsWithGenres ( [ genre , newGenre ] )
for ( const libraryItem of libraryItemsWithGenre ) {
2023-08-14 00:45:53 +02:00
if ( libraryItem . media . genres . includes ( newGenre ) ) {
2023-08-13 20:10:34 +02:00
genreMerged = true // new genre is an existing genre so this is a merge
}
2022-12-18 21:52:53 +01:00
2023-08-14 00:45:53 +02:00
if ( libraryItem . media . genres . includes ( genre ) ) {
2024-08-10 22:46:04 +02:00
libraryItem . media . genres = libraryItem . media . genres . filter ( ( t ) => t !== genre ) // Remove old genre
2023-08-14 00:45:53 +02:00
if ( ! libraryItem . media . genres . includes ( newGenre ) ) {
libraryItem . media . genres . push ( newGenre )
2022-12-18 21:52:53 +01:00
}
2023-08-13 20:10:34 +02:00
Logger . debug ( ` [MiscController] Rename genre " ${ genre } " to " ${ newGenre } " for item " ${ libraryItem . media . title } " ` )
await libraryItem . media . update ( {
2023-08-14 00:45:53 +02:00
genres : libraryItem . media . genres
2023-08-13 20:10:34 +02:00
} )
2024-04-13 00:34:10 +02:00
await libraryItem . saveMetadataFile ( )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-13 20:10:34 +02:00
SocketAuthority . emitter ( 'item_updated' , oldLibraryItem . toJSONExpanded ( ) )
2022-12-18 21:52:53 +01:00
numItemsUpdated ++
}
}
res . json ( {
genreMerged ,
numItemsUpdated
} )
}
2023-08-13 20:10:34 +02:00
/ * *
* DELETE : / a p i / g e n r e s / : g e n r e
* Remove a genre
* : genre param is base64 encoded
2024-08-11 22:15:34 +02:00
*
* @ param { RequestWithUser } req
* @ param { Response } res
2023-08-13 20:10:34 +02:00
* /
2022-12-18 21:52:53 +01:00
async deleteGenre ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to deleteGenre ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2022-12-18 21:52:53 +01:00
}
const genre = Buffer . from ( decodeURIComponent ( req . params . genre ) , 'base64' ) . toString ( )
2023-08-13 22:10:26 +02:00
// Update filter data
Database . removeGenreFromFilterData ( genre )
2023-08-13 20:10:34 +02:00
// Get all items with genre
const libraryItemsWithGenre = await libraryItemFilters . getAllLibraryItemsWithGenres ( [ genre ] )
2022-12-18 21:52:53 +01:00
let numItemsUpdated = 0
2023-08-13 20:10:34 +02:00
// Remove genre from items
for ( const libraryItem of libraryItemsWithGenre ) {
Logger . debug ( ` [MiscController] Remove genre " ${ genre } " from item " ${ libraryItem . media . title } " ` )
2024-08-10 22:46:04 +02:00
libraryItem . media . genres = libraryItem . media . genres . filter ( ( g ) => g !== genre )
2023-08-13 20:10:34 +02:00
await libraryItem . media . update ( {
2023-08-14 00:45:53 +02:00
genres : libraryItem . media . genres
2023-08-13 20:10:34 +02:00
} )
2024-04-13 00:34:10 +02:00
await libraryItem . saveMetadataFile ( )
2023-08-20 20:34:03 +02:00
const oldLibraryItem = Database . libraryItemModel . getOldLibraryItem ( libraryItem )
2023-08-13 20:10:34 +02:00
SocketAuthority . emitter ( 'item_updated' , oldLibraryItem . toJSONExpanded ( ) )
numItemsUpdated ++
2022-12-18 21:52:53 +01:00
}
res . json ( {
numItemsUpdated
} )
}
2023-10-24 15:35:43 +02:00
/ * *
2023-10-26 23:41:54 +02:00
* POST : / a p i / w a t c h e r / u p d a t e
* Update a watch path
2024-08-10 22:46:04 +02:00
* Req . body { libraryId , path , type , [ oldPath ] }
2023-10-26 23:41:54 +02:00
* type = add , unlink , rename
* oldPath = required only for rename
2024-08-11 22:15:34 +02:00
*
2023-10-26 23:41:54 +02:00
* @ this import ( '../routers/ApiRouter' )
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-10-26 23:41:54 +02:00
* /
2023-10-24 15:35:43 +02:00
updateWatchedPath ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to updateWatchedPath ` )
2024-08-11 22:15:34 +02:00
return res . sendStatus ( 403 )
2023-10-24 15:35:43 +02:00
}
const libraryId = req . body . libraryId
const path = req . body . path
const type = req . body . type
if ( ! libraryId || ! path || ! type ) {
Logger . error ( ` [MiscController] Invalid request body for updateWatchedPath. libraryId: " ${ libraryId } ", path: " ${ path } ", type: " ${ type } " ` )
return res . sendStatus ( 400 )
}
switch ( type ) {
case 'add' :
2024-11-07 23:32:05 +01:00
Watcher . onFileAdded ( libraryId , path )
2023-11-19 19:57:17 +01:00
break
2023-10-24 15:35:43 +02:00
case 'unlink' :
2024-11-07 23:32:05 +01:00
Watcher . onFileRemoved ( libraryId , path )
2023-11-19 19:57:17 +01:00
break
2023-10-24 15:35:43 +02:00
case 'rename' :
const oldPath = req . body . oldPath
if ( ! oldPath ) {
Logger . error ( ` [MiscController] Invalid request body for updateWatchedPath. oldPath is required for rename. ` )
return res . sendStatus ( 400 )
}
2024-11-07 23:32:05 +01:00
Watcher . onFileRename ( libraryId , oldPath , path )
2023-11-19 19:57:17 +01:00
break
2023-10-24 15:35:43 +02:00
default :
Logger . error ( ` [MiscController] Invalid type for updateWatchedPath. type: " ${ type } " ` )
return res . sendStatus ( 400 )
}
res . sendStatus ( 200 )
}
2022-08-02 01:06:22 +02:00
validateCronExpression ( req , res ) {
const expression = req . body . expression
if ( ! expression ) {
return res . sendStatus ( 400 )
}
try {
patternValidation ( expression )
res . sendStatus ( 200 )
} catch ( error ) {
Logger . warn ( ` [MiscController] Invalid cron expression ${ expression } ` , error . message )
res . status ( 400 ) . send ( error . message )
}
}
2023-09-24 22:36:35 +02:00
/ * *
* GET : api / auth - settings ( admin only )
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-09-24 22:36:35 +02:00
* /
getAuthSettings ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to get auth settings ` )
2023-09-24 22:36:35 +02:00
return res . sendStatus ( 403 )
}
return res . json ( Database . serverSettings . authenticationSettings )
}
2023-11-10 23:11:51 +01:00
/ * *
* PATCH : api / auth - settings
* @ this import ( '../routers/ApiRouter' )
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-11-10 23:11:51 +01:00
* /
async updateAuthSettings ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to update auth settings ` )
2023-11-10 23:11:51 +01:00
return res . sendStatus ( 403 )
}
const settingsUpdate = req . body
if ( ! isObject ( settingsUpdate ) ) {
return res . status ( 400 ) . send ( 'Invalid auth settings update object' )
}
let hasUpdates = false
const currentAuthenticationSettings = Database . serverSettings . authenticationSettings
const originalAuthMethods = [ ... currentAuthenticationSettings . authActiveAuthMethods ]
// TODO: Better validation of auth settings once auth settings are separated from server settings
for ( const key in currentAuthenticationSettings ) {
if ( settingsUpdate [ key ] === undefined ) continue
if ( key === 'authActiveAuthMethods' ) {
let updatedAuthMethods = settingsUpdate [ key ] ? . filter ? . ( ( authMeth ) => Database . serverSettings . supportedAuthMethods . includes ( authMeth ) )
if ( Array . isArray ( updatedAuthMethods ) && updatedAuthMethods . length ) {
updatedAuthMethods . sort ( )
currentAuthenticationSettings [ key ] . sort ( )
if ( updatedAuthMethods . join ( ) !== currentAuthenticationSettings [ key ] . join ( ) ) {
Logger . debug ( ` [MiscController] Updating auth settings key "authActiveAuthMethods" from " ${ currentAuthenticationSettings [ key ] . join ( ) } " to " ${ updatedAuthMethods . join ( ) } " ` )
Database . serverSettings [ key ] = updatedAuthMethods
hasUpdates = true
}
} else {
Logger . warn ( ` [MiscController] Invalid value for authActiveAuthMethods ` )
}
2023-12-04 22:36:34 +01:00
} else if ( key === 'authOpenIDMobileRedirectURIs' ) {
function isValidRedirectURI ( uri ) {
2023-12-08 00:01:33 +01:00
if ( typeof uri !== 'string' ) return false
2024-01-25 11:49:10 +01:00
const pattern = new RegExp ( '^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$' , 'i' )
2023-12-08 00:01:33 +01:00
return pattern . test ( uri )
2023-12-04 22:36:34 +01:00
}
const uris = settingsUpdate [ key ]
2024-08-10 22:46:04 +02:00
if ( ! Array . isArray ( uris ) || ( uris . includes ( '*' ) && uris . length > 1 ) || uris . some ( ( uri ) => uri !== '*' && ! isValidRedirectURI ( uri ) ) ) {
2023-12-04 22:36:34 +01:00
Logger . warn ( ` [MiscController] Invalid value for authOpenIDMobileRedirectURIs ` )
continue
}
// Update the URIs
2024-08-10 22:46:04 +02:00
if ( Database . serverSettings [ key ] . some ( ( uri ) => ! uris . includes ( uri ) ) || uris . some ( ( uri ) => ! Database . serverSettings [ key ] . includes ( uri ) ) ) {
2023-12-08 00:01:33 +01:00
Logger . debug ( ` [MiscController] Updating auth settings key " ${ key } " from " ${ Database . serverSettings [ key ] } " to " ${ uris } " ` )
Database . serverSettings [ key ] = uris
hasUpdates = true
}
2023-11-10 23:11:51 +01:00
} else {
const updatedValueType = typeof settingsUpdate [ key ]
if ( [ 'authOpenIDAutoLaunch' , 'authOpenIDAutoRegister' ] . includes ( key ) ) {
if ( updatedValueType !== 'boolean' ) {
Logger . warn ( ` [MiscController] Invalid value for ${ key } . Expected boolean ` )
continue
}
2023-11-11 20:10:24 +01:00
} else if ( settingsUpdate [ key ] !== null && updatedValueType !== 'string' ) {
2023-11-10 23:11:51 +01:00
Logger . warn ( ` [MiscController] Invalid value for ${ key } . Expected string or null ` )
continue
}
let updatedValue = settingsUpdate [ key ]
2024-11-29 03:28:50 +01:00
if ( updatedValue === '' && key != 'authOpenIDSubfolderForRedirectURLs' ) updatedValue = null
2023-11-10 23:11:51 +01:00
let currentValue = currentAuthenticationSettings [ key ]
2024-11-29 03:28:50 +01:00
if ( currentValue === '' && key != 'authOpenIDSubfolderForRedirectURLs' ) currentValue = null
2023-11-10 23:11:51 +01:00
if ( updatedValue !== currentValue ) {
Logger . debug ( ` [MiscController] Updating auth settings key " ${ key } " from " ${ currentValue } " to " ${ updatedValue } " ` )
Database . serverSettings [ key ] = updatedValue
hasUpdates = true
}
}
}
if ( hasUpdates ) {
2023-11-19 19:57:17 +01:00
await Database . updateServerSettings ( )
2023-11-10 23:11:51 +01:00
// Use/unuse auth methods
Database . serverSettings . supportedAuthMethods . forEach ( ( authMethod ) => {
if ( originalAuthMethods . includes ( authMethod ) && ! Database . serverSettings . authActiveAuthMethods . includes ( authMethod ) ) {
// Auth method has been removed
Logger . info ( ` [MiscController] Disabling active auth method " ${ authMethod } " ` )
this . auth . unuseAuthStrategy ( authMethod )
} else if ( ! originalAuthMethods . includes ( authMethod ) && Database . serverSettings . authActiveAuthMethods . includes ( authMethod ) ) {
// Auth method has been added
Logger . info ( ` [MiscController] Enabling active auth method " ${ authMethod } " ` )
this . auth . useAuthStrategy ( authMethod )
}
} )
}
res . json ( {
2023-12-08 00:01:33 +01:00
updated : hasUpdates ,
2023-11-10 23:11:51 +01:00
serverSettings : Database . serverSettings . toJSONForBrowser ( )
} )
}
2023-12-23 00:01:07 +01:00
/ * *
2024-02-15 23:46:19 +01:00
* GET : / a p i / s t a t s / y e a r / : y e a r
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2023-12-23 00:01:07 +01:00
* /
async getAdminStatsForYear ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to get admin stats for year ` )
2023-12-23 00:01:07 +01:00
return res . sendStatus ( 403 )
}
const year = Number ( req . params . year )
if ( isNaN ( year ) || year < 2000 || year > 9999 ) {
Logger . error ( ` [MiscController] Invalid year " ${ year } " ` )
return res . status ( 400 ) . send ( 'Invalid year' )
}
const stats = await adminStats . getStatsForYear ( year )
res . json ( stats )
}
2024-02-15 23:46:19 +01:00
/ * *
* GET : / a p i / l o g g e r - d a t a
* admin or up
2024-08-10 22:46:04 +02:00
*
2024-08-11 22:15:34 +02:00
* @ param { RequestWithUser } req
* @ param { Response } res
2024-02-15 23:46:19 +01:00
* /
async getLoggerData ( req , res ) {
2024-08-11 23:07:29 +02:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [MiscController] Non-admin user " ${ req . user . username } " attempted to get logger data ` )
2024-02-15 23:46:19 +01:00
return res . sendStatus ( 403 )
}
res . json ( {
currentDailyLogs : Logger . logManager . getMostRecentCurrentDailyLogs ( )
} )
}
2022-03-18 17:51:55 +01:00
}
2023-12-02 06:42:54 +01:00
module . exports = new MiscController ( )