Add music/book request flow support
- Add permission checks for music/book in MediaRequest.request() - Add quota checks for music/book types - Add externalServiceId and externalServiceTitle columns to Media entity - Add foreignId/foreignTitle to MediaRequestBody interface - Add requestMusicOrBook() method for simplified music/book requests - Make TMDB lookup conditional (skip for music/book) - Update request route filtering for music/book types - Handle duplicate detection for foreign ID based media
This commit is contained in:
@@ -98,6 +98,13 @@ class Media {
|
||||
@Index()
|
||||
public imdbId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Index()
|
||||
public externalServiceId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public externalServiceTitle?: string;
|
||||
|
||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||
@Index()
|
||||
public status: MediaStatus;
|
||||
|
||||
@@ -108,6 +108,26 @@ export class MediaRequest {
|
||||
requestBody.is4k ? '4K ' : ''
|
||||
}series requests.`
|
||||
);
|
||||
} else if (
|
||||
requestBody.mediaType === MediaType.MUSIC &&
|
||||
!requestUser.hasPermission(
|
||||
[Permission.REQUEST, Permission.REQUEST_MUSIC],
|
||||
{ type: 'or' }
|
||||
)
|
||||
) {
|
||||
throw new RequestPermissionError(
|
||||
'You do not have permission to make music requests.'
|
||||
);
|
||||
} else if (
|
||||
requestBody.mediaType === MediaType.BOOK &&
|
||||
!requestUser.hasPermission(
|
||||
[Permission.REQUEST, Permission.REQUEST_BOOK],
|
||||
{ type: 'or' }
|
||||
)
|
||||
) {
|
||||
throw new RequestPermissionError(
|
||||
'You do not have permission to make book requests.'
|
||||
);
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
@@ -116,25 +136,42 @@ export class MediaRequest {
|
||||
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||
} else if (requestBody.mediaType === MediaType.MUSIC && quotas.music?.restricted) {
|
||||
throw new QuotaRestrictedError('Music Quota exceeded.');
|
||||
} else if (requestBody.mediaType === MediaType.BOOK && quotas.book?.restricted) {
|
||||
throw new QuotaRestrictedError('Book Quota exceeded.');
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
||||
// Music and Book requests don't use TMDB - they use foreign IDs from Lidarr/Readarr
|
||||
const isMusicOrBook = requestBody.mediaType === MediaType.MUSIC || requestBody.mediaType === MediaType.BOOK;
|
||||
|
||||
let tmdbMedia: any = null;
|
||||
if (!isMusicOrBook) {
|
||||
tmdbMedia =
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
||||
}
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: {
|
||||
tmdbId: requestBody.mediaId,
|
||||
mediaType: requestBody.mediaType,
|
||||
},
|
||||
where: isMusicOrBook
|
||||
? {
|
||||
externalServiceId: requestBody.foreignId,
|
||||
mediaType: requestBody.mediaType,
|
||||
}
|
||||
: {
|
||||
tmdbId: requestBody.mediaId,
|
||||
mediaType: requestBody.mediaType,
|
||||
},
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: tmdbMedia.id,
|
||||
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||
tmdbId: isMusicOrBook ? 0 : tmdbMedia.id,
|
||||
tvdbId: isMusicOrBook ? 0 : (requestBody.tvdbId ?? tmdbMedia.external_ids?.tvdb_id),
|
||||
externalServiceId: isMusicOrBook ? requestBody.foreignId : undefined,
|
||||
externalServiceTitle: isMusicOrBook ? requestBody.foreignTitle : undefined,
|
||||
status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
mediaType: requestBody.mediaType,
|
||||
@@ -159,16 +196,26 @@ export class MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await requestRepository
|
||||
const existingQuery = requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.media', 'media')
|
||||
.leftJoinAndSelect('request.requestedBy', 'user')
|
||||
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
|
||||
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
||||
.andWhere('media.mediaType = :mediaType', {
|
||||
mediaType: requestBody.mediaType,
|
||||
})
|
||||
.getMany();
|
||||
});
|
||||
|
||||
if (isMusicOrBook) {
|
||||
existingQuery.andWhere('media.externalServiceId = :foreignId', {
|
||||
foreignId: requestBody.foreignId,
|
||||
});
|
||||
} else {
|
||||
existingQuery.andWhere('media.tmdbId = :tmdbId', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
});
|
||||
}
|
||||
|
||||
const existing = await existingQuery.getMany();
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
// If there is an existing movie request that isn't declined, don't allow a new one.
|
||||
@@ -510,6 +557,72 @@ export class MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle music/book request creation (simpler flow - no TMDB, no seasons, no 4K)
|
||||
*/
|
||||
public static async requestMusicOrBook(
|
||||
requestBody: MediaRequestBody,
|
||||
user: User,
|
||||
options: MediaRequestOptions = {}
|
||||
): Promise<MediaRequest> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: {
|
||||
externalServiceId: requestBody.foreignId,
|
||||
mediaType: requestBody.mediaType,
|
||||
},
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: 0,
|
||||
tvdbId: 0,
|
||||
externalServiceId: requestBody.foreignId,
|
||||
externalServiceTitle: requestBody.foreignTitle,
|
||||
status: MediaStatus.PENDING,
|
||||
status4k: MediaStatus.UNKNOWN,
|
||||
mediaType: requestBody.mediaType,
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const autoApprovePermission =
|
||||
requestBody.mediaType === MediaType.MUSIC
|
||||
? Permission.AUTO_APPROVE_MUSIC
|
||||
: Permission.AUTO_APPROVE_BOOK;
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: requestBody.mediaType,
|
||||
media,
|
||||
requestedBy: user,
|
||||
status: user.hasPermission(
|
||||
[Permission.AUTO_APPROVE, autoApprovePermission, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: user.hasPermission(
|
||||
[Permission.AUTO_APPROVE, autoApprovePermission, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? user
|
||||
: undefined,
|
||||
is4k: false,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
tags: requestBody.tags,
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user