import {
MediaRequestStatus ,
MediaStatus ,
MediaType ,
} from '@server/constants/media' ;
import { getRepository } from '@server/datasource' ;
import Media from '@server/entity/Media' ;
import {
DuplicateMediaRequestError ,
MediaRequest ,
NoSeasonsAvailableError ,
QuotaRestrictedError ,
RequestPermissionError ,
} from '@server/entity/MediaRequest' ;
import SeasonRequest from '@server/entity/SeasonRequest' ;
import { User } from '@server/entity/User' ;
import type {
MediaRequestBody ,
RequestResultsResponse ,
} from '@server/interfaces/api/requestInterfaces' ;
import { Permission } from '@server/lib/permissions' ;
import logger from '@server/logger' ;
import { isAuthenticated } from '@server/middleware/auth' ;
import { Router } from 'express' ;
const requestRoutes = Router ( ) ;
requestRoutes . get < Record < string , unknown > , RequestResultsResponse > (
'/' ,
async ( req , res , next ) = > {
try {
const pageSize = req . query . take ? Number ( req . query . take ) : 10 ;
const skip = req . query . skip ? Number ( req . query . skip ) : 0 ;
const requestedBy = req . query . requestedBy
? Number ( req . query . requestedBy )
: null ;
let statusFilter : MediaRequestStatus [ ] ;
switch ( req . query . filter ) {
case 'approved' :
case 'processing' :
case 'available' :
statusFilter = [ MediaRequestStatus . APPROVED ] ;
break ;
case 'pending' :
statusFilter = [ MediaRequestStatus . PENDING ] ;
break ;
case 'unavailable' :
statusFilter = [
MediaRequestStatus . PENDING ,
MediaRequestStatus . APPROVED ,
] ;
break ;
case 'failed' :
statusFilter = [ MediaRequestStatus . FAILED ] ;
break ;
default :
statusFilter = [
MediaRequestStatus . PENDING ,
MediaRequestStatus . APPROVED ,
MediaRequestStatus . DECLINED ,
MediaRequestStatus . FAILED ,
] ;
}
let mediaStatusFilter : MediaStatus [ ] ;
switch ( req . query . filter ) {
case 'available' :
mediaStatusFilter = [ MediaStatus . AVAILABLE ] ;
break ;
case 'processing' :
case 'unavailable' :
mediaStatusFilter = [
MediaStatus . UNKNOWN ,
MediaStatus . PENDING ,
MediaStatus . PROCESSING ,
MediaStatus . PARTIALLY_AVAILABLE ,
] ;
break ;
default :
mediaStatusFilter = [
MediaStatus . UNKNOWN ,
MediaStatus . PENDING ,
MediaStatus . PROCESSING ,
MediaStatus . PARTIALLY_AVAILABLE ,
MediaStatus . AVAILABLE ,
] ;
}
let sortFilter : string ;
switch ( req . query . sort ) {
case 'modified' :
sortFilter = 'request.updatedAt' ;
break ;
default :
sortFilter = 'request.id' ;
}
let query = getRepository ( MediaRequest )
. createQueryBuilder ( 'request' )
. leftJoinAndSelect ( 'request.media' , 'media' )
. leftJoinAndSelect ( 'request.seasons' , 'seasons' )
. leftJoinAndSelect ( 'request.modifiedBy' , 'modifiedBy' )
. leftJoinAndSelect ( 'request.requestedBy' , 'requestedBy' )
. where ( 'request.status IN (:...requestStatus)' , {
requestStatus : statusFilter ,
} )
. andWhere (
'((request.is4k = 0 AND media.status IN (:...mediaStatus)) OR (request.is4k = 1 AND media.status4k IN (:...mediaStatus)))' ,
{
mediaStatus : mediaStatusFilter ,
}
) ;
if (
! req . user ? . hasPermission (
[ Permission . MANAGE_REQUESTS , Permission . REQUEST_VIEW ] ,
{ type : 'or' }
)
) {
if ( requestedBy && requestedBy !== req . user ? . id ) {
return next ( {
status : 403 ,
message : "You do not have permission to view this user's requests." ,
} ) ;
}
query = query . andWhere ( 'requestedBy.id = :id' , {
id : req.user?.id ,
} ) ;
} else if ( requestedBy ) {
query = query . andWhere ( 'requestedBy.id = :id' , {
id : requestedBy ,
} ) ;
}
const [ requests , requestCount ] = await query
. orderBy ( sortFilter , 'DESC' )
. take ( pageSize )
. skip ( skip )
. getManyAndCount ( ) ;
return res . status ( 200 ) . json ( {
pageInfo : {
pages : Math.ceil ( requestCount / pageSize ) ,
pageSize ,
results : requestCount ,
page : Math.ceil ( skip / pageSize ) + 1 ,
} ,
results : requests ,
} ) ;
} catch ( e ) {
next ( { status : 500 , message : e.message } ) ;
}
}
) ;
requestRoutes . post < never , MediaRequest , MediaRequestBody > (
'/' ,
async ( req , res , next ) = > {
try {
if ( ! req . user ) {
return next ( {
status : 401 ,
message : 'You must be logged in to request media.' ,
} ) ;
}
const request = await MediaRequest . request ( req . body , req . user ) ;
return res . status ( 201 ) . json ( request ) ;
} catch ( error ) {
if ( ! ( error instanceof Error ) ) {
return ;
}
switch ( error . constructor ) {
case RequestPermissionError :
case QuotaRestrictedError :
return next ( { status : 403 , message : error.message } ) ;
case DuplicateMediaRequestError :
return next ( { status : 409 , message : error.message } ) ;
case NoSeasonsAvailableError :
return next ( { status : 202 , message : error.message } ) ;
default :
return next ( { status : 500 , message : error.message } ) ;
}
}
}
) ;
requestRoutes . get ( '/count' , async ( _req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
try {
const query = requestRepository
. createQueryBuilder ( 'request' )
. leftJoinAndSelect ( 'request.media' , 'media' ) ;
const totalCount = await query . getCount ( ) ;
const movieCount = await query
. where ( 'request.type = :requestType' , {
requestType : MediaType.MOVIE ,
} )
. getCount ( ) ;
const tvCount = await query
. where ( 'request.type = :requestType' , {
requestType : MediaType.TV ,
} )
. getCount ( ) ;
const pendingCount = await query
. where ( 'request.status = :requestStatus' , {
requestStatus : MediaRequestStatus.PENDING ,
} )
. getCount ( ) ;
const approvedCount = await query
. where ( 'request.status = :requestStatus' , {
requestStatus : MediaRequestStatus.APPROVED ,
} )
. getCount ( ) ;
const declinedCount = await query
. where ( 'request.status = :requestStatus' , {
requestStatus : MediaRequestStatus.DECLINED ,
} )
. getCount ( ) ;
const processingCount = await query
. where ( 'request.status = :requestStatus' , {
requestStatus : MediaRequestStatus.APPROVED ,
} )
. andWhere (
'((request.is4k = false AND media.status != :availableStatus) OR (request.is4k = true AND media.status4k != :availableStatus))' ,
{
availableStatus : MediaStatus.AVAILABLE ,
}
)
. getCount ( ) ;
const availableCount = await query
. where ( 'request.status = :requestStatus' , {
requestStatus : MediaRequestStatus.APPROVED ,
} )
. andWhere (
'((request.is4k = false AND media.status = :availableStatus) OR (request.is4k = true AND media.status4k = :availableStatus))' ,
{
availableStatus : MediaStatus.AVAILABLE ,
}
)
. getCount ( ) ;
return res . status ( 200 ) . json ( {
total : totalCount ,
movie : movieCount ,
tv : tvCount ,
pending : pendingCount ,
approved : approvedCount ,
declined : declinedCount ,
processing : processingCount ,
available : availableCount ,
} ) ;
} catch ( e ) {
logger . error ( 'Something went wrong retrieving request counts' , {
label : 'API' ,
errorMessage : e.message ,
} ) ;
next ( { status : 500 , message : 'Unable to retrieve request counts.' } ) ;
}
} ) ;
requestRoutes . get ( '/:requestId' , async ( req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
try {
const request = await requestRepository . findOneOrFail ( {
where : { id : Number ( req . params . requestId ) } ,
relations : { requestedBy : true , modifiedBy : true } ,
} ) ;
if (
request . requestedBy . id !== req . user ? . id &&
! req . user ? . hasPermission (
[ Permission . MANAGE_REQUESTS , Permission . REQUEST_VIEW ] ,
{ type : 'or' }
)
) {
return next ( {
status : 403 ,
message : 'You do not have permission to view this request.' ,
} ) ;
}
return res . status ( 200 ) . json ( request ) ;
} catch ( e ) {
logger . debug ( 'Failed to retrieve request.' , {
label : 'API' ,
errorMessage : e.message ,
} ) ;
next ( { status : 404 , message : 'Request not found.' } ) ;
}
} ) ;
requestRoutes . put < { requestId : string } > (
'/:requestId' ,
async ( req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
const userRepository = getRepository ( User ) ;
try {
const request = await requestRepository . findOne ( {
where : {
id : Number ( req . params . requestId ) ,
} ,
} ) ;
if ( ! request ) {
return next ( { status : 404 , message : 'Request not found.' } ) ;
}
if (
( request . requestedBy . id !== req . user ? . id ||
( req . body . mediaType !== 'tv' &&
! req . user ? . hasPermission ( Permission . REQUEST_ADVANCED ) ) ) &&
! req . user ? . hasPermission ( Permission . MANAGE_REQUESTS )
) {
return next ( {
status : 403 ,
message : 'You do not have permission to modify this request.' ,
} ) ;
}
let requestUser = request . requestedBy ;
if (
req . body . userId &&
req . body . userId !== request . requestedBy . id &&
! req . user ? . hasPermission ( [
Permission . MANAGE_USERS ,
Permission . MANAGE_REQUESTS ,
] )
) {
return next ( {
status : 403 ,
message : 'You do not have permission to modify the request user.' ,
} ) ;
} else if ( req . body . userId ) {
requestUser = await userRepository . findOneOrFail ( {
where : { id : req.body.userId } ,
} ) ;
}
if ( req . body . mediaType === MediaType . MOVIE ) {
request . serverId = req . body . serverId ;
request . profileId = req . body . profileId ;
request . rootFolder = req . body . rootFolder ;
request . tags = req . body . tags ;
request . requestedBy = requestUser as User ;
requestRepository . save ( request ) ;
} else if ( req . body . mediaType === MediaType . TV ) {
const mediaRepository = getRepository ( Media ) ;
request . serverId = req . body . serverId ;
request . profileId = req . body . profileId ;
request . rootFolder = req . body . rootFolder ;
request . languageProfileId = req . body . languageProfileId ;
request . tags = req . body . tags ;
request . requestedBy = requestUser as User ;
const requestedSeasons = req . body . seasons as number [ ] | undefined ;
if ( ! requestedSeasons || requestedSeasons . length === 0 ) {
throw new Error (
'Missing seasons. If you want to cancel a series request, use the DELETE method.'
) ;
}
// Get existing media so we can work with all the requests
const media = await mediaRepository . findOneOrFail ( {
where : { tmdbId : request.media.tmdbId , mediaType : MediaType.TV } ,
relations : { requests : true } ,
} ) ;
// Get all requested seasons that are not part of this request we are editing
const existingSeasons = media . requests
. filter (
( r ) = >
r . is4k === request . is4k &&
r . id !== request . id &&
r . status !== MediaRequestStatus . DECLINED
)
. reduce ( ( seasons , r ) = > {
const combinedSeasons = r . seasons . map (
( season ) = > season . seasonNumber
) ;
return [ . . . seasons , . . . combinedSeasons ] ;
} , [ ] as number [ ] ) ;
const filteredSeasons = requestedSeasons . filter (
( rs ) = > ! existingSeasons . includes ( rs )
) ;
if ( filteredSeasons . length === 0 ) {
return next ( {
status : 202 ,
message : 'No seasons available to request' ,
} ) ;
}
const newSeasons = requestedSeasons . filter (
( sn ) = > ! request . seasons . map ( ( s ) = > s . seasonNumber ) . includes ( sn )
) ;
request . seasons = request . seasons . filter ( ( rs ) = >
filteredSeasons . includes ( rs . seasonNumber )
) ;
if ( newSeasons . length > 0 ) {
logger . debug ( 'Adding new seasons to request' , {
label : 'Media Request' ,
newSeasons ,
} ) ;
request . seasons . push (
. . . newSeasons . map (
( ns ) = >
new SeasonRequest ( {
seasonNumber : ns ,
status : MediaRequestStatus.PENDING ,
} )
)
) ;
}
await requestRepository . save ( request ) ;
}
return res . status ( 200 ) . json ( request ) ;
} catch ( e ) {
next ( { status : 500 , message : e.message } ) ;
}
}
) ;
requestRoutes . delete ( '/:requestId' , async ( req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
try {
const request = await requestRepository . findOneOrFail ( {
where : { id : Number ( req . params . requestId ) } ,
relations : { requestedBy : true , modifiedBy : true } ,
} ) ;
if (
! req . user ? . hasPermission ( Permission . MANAGE_REQUESTS ) &&
request . requestedBy . id !== req . user ? . id &&
request . status !== 1
) {
return next ( {
status : 401 ,
message : 'You do not have permission to delete this request.' ,
} ) ;
}
await requestRepository . remove ( request ) ;
return res . status ( 204 ) . send ( ) ;
} catch ( e ) {
logger . error ( 'Something went wrong deleting a request.' , {
label : 'API' ,
errorMessage : e.message ,
} ) ;
next ( { status : 404 , message : 'Request not found.' } ) ;
}
} ) ;
requestRoutes . post < {
requestId : string ;
} > (
'/:requestId/retry' ,
isAuthenticated ( Permission . MANAGE_REQUESTS ) ,
async ( req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
try {
const request = await requestRepository . findOneOrFail ( {
where : { id : Number ( req . params . requestId ) } ,
relations : { requestedBy : true , modifiedBy : true } ,
} ) ;
await request . updateParentStatus ( ) ;
await request . sendMedia ( ) ;
return res . status ( 200 ) . json ( request ) ;
} catch ( e ) {
logger . error ( 'Error processing request retry' , {
label : 'Media Request' ,
message : e.message ,
} ) ;
next ( { status : 404 , message : 'Request not found.' } ) ;
}
}
) ;
requestRoutes . post < {
requestId : string ;
status : 'pending' | 'approve' | 'decline' ;
} > (
'/:requestId/:status' ,
isAuthenticated ( Permission . MANAGE_REQUESTS ) ,
async ( req , res , next ) = > {
const requestRepository = getRepository ( MediaRequest ) ;
try {
const request = await requestRepository . findOneOrFail ( {
where : { id : Number ( req . params . requestId ) } ,
relations : { requestedBy : true , modifiedBy : true } ,
} ) ;
let newStatus : MediaRequestStatus ;
switch ( req . params . status ) {
case 'pending' :
newStatus = MediaRequestStatus . PENDING ;
break ;
case 'approve' :
newStatus = MediaRequestStatus . APPROVED ;
break ;
case 'decline' :
newStatus = MediaRequestStatus . DECLINED ;
break ;
}
request . status = newStatus ;
request . modifiedBy = req . user ;
await requestRepository . save ( request ) ;
return res . status ( 200 ) . json ( request ) ;
} catch ( e ) {
logger . error ( 'Error processing request update' , {
label : 'Media Request' ,
message : e.message ,
} ) ;
next ( { status : 404 , message : 'Request not found.' } ) ;
}
}
) ;
export default requestRoutes ;