import { Router } from 'express' ;
import rateLimit from 'express-rate-limit' ;
import fs from 'fs' ;
import { merge , omit } from 'lodash' ;
import path from 'path' ;
import { getRepository } from 'typeorm' ;
import PlexAPI from '../../api/plexapi' ;
import PlexTvAPI from '../../api/plextv' ;
import Media from '../../entity/Media' ;
import { MediaRequest } from '../../entity/MediaRequest' ;
import { User } from '../../entity/User' ;
import {
LogMessage ,
LogsResultsResponse ,
SettingsAboutResponse ,
} from '../../interfaces/api/settingsInterfaces' ;
import { scheduledJobs } from '../../job/schedule' ;
import cacheManager , { AvailableCacheIds } from '../../lib/cache' ;
import { Permission } from '../../lib/permissions' ;
import { plexFullScanner } from '../../lib/scanners/plex' ;
import { getSettings , Library , MainSettings } from '../../lib/settings' ;
import logger from '../../logger' ;
import { isAuthenticated } from '../../middleware/auth' ;
import { getAppVersion } from '../../utils/appVersion' ;
import notificationRoutes from './notifications' ;
import radarrRoutes from './radarr' ;
import sonarrRoutes from './sonarr' ;
const settingsRoutes = Router ( ) ;
settingsRoutes . use ( '/notifications' , notificationRoutes ) ;
settingsRoutes . use ( '/radarr' , radarrRoutes ) ;
settingsRoutes . use ( '/sonarr' , sonarrRoutes ) ;
const filteredMainSettings = (
user : User ,
main : MainSettings
) : Partial < MainSettings > = > {
if ( ! user ? . hasPermission ( Permission . ADMIN ) ) {
return omit ( main , 'apiKey' ) ;
}
return main ;
} ;
settingsRoutes . get ( '/main' , ( req , res , next ) = > {
const settings = getSettings ( ) ;
if ( ! req . user ) {
return next ( { status : 500 , message : 'User missing from request' } ) ;
}
res . status ( 200 ) . json ( filteredMainSettings ( req . user , settings . main ) ) ;
} ) ;
settingsRoutes . post ( '/main' , ( req , res ) = > {
const settings = getSettings ( ) ;
settings . main = merge ( settings . main , req . body ) ;
settings . save ( ) ;
return res . status ( 200 ) . json ( settings . main ) ;
} ) ;
settingsRoutes . post ( '/main/regenerate' , ( req , res , next ) = > {
const settings = getSettings ( ) ;
const main = settings . regenerateApiKey ( ) ;
if ( ! req . user ) {
return next ( { status : 500 , message : 'User missing from request' } ) ;
}
return res . status ( 200 ) . json ( filteredMainSettings ( req . user , main ) ) ;
} ) ;
settingsRoutes . get ( '/plex' , ( _req , res ) = > {
const settings = getSettings ( ) ;
res . status ( 200 ) . json ( settings . plex ) ;
} ) ;
settingsRoutes . post ( '/plex' , async ( req , res , next ) = > {
const userRepository = getRepository ( User ) ;
const settings = getSettings ( ) ;
try {
const admin = await userRepository . findOneOrFail ( {
select : [ 'id' , 'plexToken' ] ,
order : { id : 'ASC' } ,
} ) ;
Object . assign ( settings . plex , req . body ) ;
const plexClient = new PlexAPI ( { plexToken : admin.plexToken } ) ;
const result = await plexClient . getStatus ( ) ;
if ( result ? . MediaContainer ? . machineIdentifier ) {
settings . plex . machineId = result . MediaContainer . machineIdentifier ;
settings . plex . name = result . MediaContainer . friendlyName ;
settings . save ( ) ;
}
} catch ( e ) {
return next ( {
status : 500 ,
message : ` Failed to connect to Plex: ${ e . message } ` ,
} ) ;
}
return res . status ( 200 ) . json ( settings . plex ) ;
} ) ;
settingsRoutes . get ( '/plex/devices/servers' , async ( req , res , next ) = > {
const userRepository = getRepository ( User ) ;
try {
const admin = await userRepository . findOneOrFail ( {
select : [ 'id' , 'plexToken' ] ,
order : { id : 'ASC' } ,
} ) ;
const plexTvClient = admin . plexToken
? new PlexTvAPI ( admin . plexToken )
: null ;
const devices = ( await plexTvClient ? . getDevices ( ) ) ? . filter ( ( device ) = > {
return device . provides . includes ( 'server' ) && device . owned ;
} ) ;
const settings = getSettings ( ) ;
if ( devices ) {
await Promise . all (
devices . map ( async ( device ) = > {
await Promise . all (
device . connection . map ( async ( connection ) = > {
const plexDeviceSettings = {
. . . settings . plex ,
ip : connection.address ,
port : connection.port ,
useSsl : connection.protocol === 'https' ? true : false ,
} ;
const plexClient = new PlexAPI ( {
plexToken : admin.plexToken ,
plexSettings : plexDeviceSettings ,
timeout : 5000 ,
} ) ;
try {
await plexClient . getStatus ( ) ;
connection . status = 200 ;
connection . message = 'OK' ;
} catch ( e ) {
connection . status = 500 ;
connection . message = e . message ;
}
} )
) ;
} )
) ;
}
return res . status ( 200 ) . json ( devices ) ;
} catch ( e ) {
return next ( {
status : 500 ,
message : ` Failed to connect to Plex: ${ e . message } ` ,
} ) ;
}
} ) ;
settingsRoutes . get ( '/plex/library' , async ( req , res ) = > {
const settings = getSettings ( ) ;
if ( req . query . sync ) {
const userRepository = getRepository ( User ) ;
const admin = await userRepository . findOneOrFail ( {
select : [ 'id' , 'plexToken' ] ,
order : { id : 'ASC' } ,
} ) ;
const plexapi = new PlexAPI ( { plexToken : admin.plexToken } ) ;
const libraries = await plexapi . getLibraries ( ) ;
const newLibraries : Library [ ] = libraries
// Remove libraries that are not movie or show
. filter ( ( library ) = > library . type === 'movie' || library . type === 'show' )
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
. filter ( ( library ) = > library . agent !== 'com.plexapp.agents.none' )
. map ( ( library ) = > {
const existing = settings . plex . libraries . find (
( l ) = > l . id === library . key && l . name === library . title
) ;
return {
id : library.key ,
name : library.title ,
enabled : existing?.enabled ? ? false ,
} ;
} ) ;
settings . plex . libraries = newLibraries ;
}
const enabledLibraries = req . query . enable
? ( req . query . enable as string ) . split ( ',' )
: [ ] ;
settings . plex . libraries = settings . plex . libraries . map ( ( library ) = > ( {
. . . library ,
enabled : enabledLibraries.includes ( library . id ) ,
} ) ) ;
settings . save ( ) ;
return res . status ( 200 ) . json ( settings . plex . libraries ) ;
} ) ;
settingsRoutes . get ( '/plex/sync' , ( _req , res ) = > {
return res . status ( 200 ) . json ( plexFullScanner . status ( ) ) ;
} ) ;
settingsRoutes . post ( '/plex/sync' , ( req , res ) = > {
if ( req . body . cancel ) {
plexFullScanner . cancel ( ) ;
} else if ( req . body . start ) {
plexFullScanner . run ( ) ;
}
return res . status ( 200 ) . json ( plexFullScanner . status ( ) ) ;
} ) ;
settingsRoutes . get (
'/logs' ,
rateLimit ( { windowMs : 60 * 1000 , max : 50 } ) ,
( req , res , next ) = > {
const pageSize = req . query . take ? Number ( req . query . take ) : 25 ;
const skip = req . query . skip ? Number ( req . query . skip ) : 0 ;
let filter : string [ ] = [ ] ;
switch ( req . query . filter ) {
case 'debug' :
filter . push ( 'debug' ) ;
// falls through
case 'info' :
filter . push ( 'info' ) ;
// falls through
case 'warn' :
filter . push ( 'warn' ) ;
// falls through
case 'error' :
filter . push ( 'error' ) ;
break ;
default :
filter = [ 'debug' , 'info' , 'warn' , 'error' ] ;
}
const logFile = process . env . CONFIG_DIRECTORY
? ` ${ process . env . CONFIG_DIRECTORY } /logs/overseerr.log `
: path . join ( __dirname , '../../../config/logs/overseerr.log' ) ;
const logs : LogMessage [ ] = [ ] ;
try {
fs . readFileSync ( logFile )
. toString ( )
. split ( '\n' )
. forEach ( ( line ) = > {
if ( ! line . length ) return ;
const timestamp = line . match ( new RegExp ( /^.{24}/ ) ) || [ ] ;
const level = line . match ( new RegExp ( /\s\[\w+\]/ ) ) || [ ] ;
const label = line . match ( new RegExp ( /\]\[.+?\]/ ) ) || [ ] ;
const message = line . match ( new RegExp ( /:\s([^{}]+)({.*})?/ ) ) || [ ] ;
if ( level . length && filter . includes ( level [ 0 ] . slice ( 2 , - 1 ) ) ) {
logs . push ( {
timestamp : timestamp [ 0 ] ,
level : level.length ? level [ 0 ] . slice ( 2 , - 1 ) : '' ,
label : label.length ? label [ 0 ] . slice ( 2 , - 1 ) : '' ,
message : message.length && message [ 1 ] ? message [ 1 ] : '' ,
data :
message . length && message [ 2 ]
? JSON . parse ( message [ 2 ] )
: undefined ,
} ) ;
}
} ) ;
const displayedLogs = logs . reverse ( ) . slice ( skip , skip + pageSize ) ;
return res . status ( 200 ) . json ( {
pageInfo : {
pages : Math.ceil ( logs . length / pageSize ) ,
pageSize ,
results : logs.length ,
page : Math.ceil ( skip / pageSize ) + 1 ,
} ,
results : displayedLogs ,
} as LogsResultsResponse ) ;
} catch ( error ) {
logger . error ( 'Something went wrong while fetching the logs' , {
label : 'Logs' ,
errorMessage : error.message ,
} ) ;
return next ( {
status : 500 ,
message : 'Something went wrong while fetching the logs' ,
} ) ;
}
}
) ;
settingsRoutes . get ( '/jobs' , ( _req , res ) = > {
return res . status ( 200 ) . json (
scheduledJobs . map ( ( job ) = > ( {
id : job.id ,
name : job.name ,
type : job . type ,
nextExecutionTime : job.job.nextInvocation ( ) ,
running : job.running ? job . running ( ) : false ,
} ) )
) ;
} ) ;
settingsRoutes . post < { jobId : string } > ( '/jobs/:jobId/run' , ( req , res , next ) = > {
const scheduledJob = scheduledJobs . find ( ( job ) = > job . id === req . params . jobId ) ;
if ( ! scheduledJob ) {
return next ( { status : 404 , message : 'Job not found' } ) ;
}
scheduledJob . job . invoke ( ) ;
return res . status ( 200 ) . json ( {
id : scheduledJob.id ,
name : scheduledJob.name ,
type : scheduledJob . type ,
nextExecutionTime : scheduledJob.job.nextInvocation ( ) ,
running : scheduledJob.running ? scheduledJob . running ( ) : false ,
} ) ;
} ) ;
settingsRoutes . post < { jobId : string } > (
'/jobs/:jobId/cancel' ,
( req , res , next ) = > {
const scheduledJob = scheduledJobs . find (
( job ) = > job . id === req . params . jobId
) ;
if ( ! scheduledJob ) {
return next ( { status : 404 , message : 'Job not found' } ) ;
}
if ( scheduledJob . cancelFn ) {
scheduledJob . cancelFn ( ) ;
}
return res . status ( 200 ) . json ( {
id : scheduledJob.id ,
name : scheduledJob.name ,
type : scheduledJob . type ,
nextExecutionTime : scheduledJob.job.nextInvocation ( ) ,
running : scheduledJob.running ? scheduledJob . running ( ) : false ,
} ) ;
}
) ;
settingsRoutes . get ( '/cache' , ( req , res ) = > {
const caches = cacheManager . getAllCaches ( ) ;
return res . status ( 200 ) . json (
Object . values ( caches ) . map ( ( cache ) = > ( {
id : cache.id ,
name : cache.name ,
stats : cache.getStats ( ) ,
} ) )
) ;
} ) ;
settingsRoutes . post < { cacheId : AvailableCacheIds } > (
'/cache/:cacheId/flush' ,
( req , res , next ) = > {
const cache = cacheManager . getCache ( req . params . cacheId ) ;
if ( cache ) {
cache . flush ( ) ;
return res . status ( 204 ) . send ( ) ;
}
next ( { status : 404 , message : 'Cache does not exist.' } ) ;
}
) ;
settingsRoutes . post (
'/initialize' ,
isAuthenticated ( Permission . ADMIN ) ,
( _req , res ) = > {
const settings = getSettings ( ) ;
settings . public . initialized = true ;
settings . save ( ) ;
return res . status ( 200 ) . json ( settings . public ) ;
}
) ;
settingsRoutes . get ( '/about' , async ( req , res ) = > {
const mediaRepository = getRepository ( Media ) ;
const mediaRequestRepository = getRepository ( MediaRequest ) ;
const totalMediaItems = await mediaRepository . count ( ) ;
const totalRequests = await mediaRequestRepository . count ( ) ;
return res . status ( 200 ) . json ( {
version : getAppVersion ( ) ,
totalMediaItems ,
totalRequests ,
tz : process.env.TZ ,
} as SettingsAboutResponse ) ;
} ) ;
export default settingsRoutes ;