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()
|
@Index()
|
||||||
public imdbId?: string;
|
public imdbId?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
@Index()
|
||||||
|
public externalServiceId?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public externalServiceTitle?: string;
|
||||||
|
|
||||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||||
@Index()
|
@Index()
|
||||||
public status: MediaStatus;
|
public status: MediaStatus;
|
||||||
|
|||||||
@@ -108,6 +108,26 @@ export class MediaRequest {
|
|||||||
requestBody.is4k ? '4K ' : ''
|
requestBody.is4k ? '4K ' : ''
|
||||||
}series requests.`
|
}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();
|
const quotas = await requestUser.getQuota();
|
||||||
@@ -116,15 +136,30 @@ export class MediaRequest {
|
|||||||
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||||
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
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 =
|
// 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
|
requestBody.mediaType === MediaType.MOVIE
|
||||||
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
||||||
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
||||||
|
}
|
||||||
|
|
||||||
let media = await mediaRepository.findOne({
|
let media = await mediaRepository.findOne({
|
||||||
where: {
|
where: isMusicOrBook
|
||||||
|
? {
|
||||||
|
externalServiceId: requestBody.foreignId,
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
}
|
||||||
|
: {
|
||||||
tmdbId: requestBody.mediaId,
|
tmdbId: requestBody.mediaId,
|
||||||
mediaType: requestBody.mediaType,
|
mediaType: requestBody.mediaType,
|
||||||
},
|
},
|
||||||
@@ -133,8 +168,10 @@ export class MediaRequest {
|
|||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
media = new Media({
|
media = new Media({
|
||||||
tmdbId: tmdbMedia.id,
|
tmdbId: isMusicOrBook ? 0 : tmdbMedia.id,
|
||||||
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_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,
|
status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||||
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||||
mediaType: requestBody.mediaType,
|
mediaType: requestBody.mediaType,
|
||||||
@@ -159,16 +196,26 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await requestRepository
|
const existingQuery = requestRepository
|
||||||
.createQueryBuilder('request')
|
.createQueryBuilder('request')
|
||||||
.leftJoin('request.media', 'media')
|
.leftJoin('request.media', 'media')
|
||||||
.leftJoinAndSelect('request.requestedBy', 'user')
|
.leftJoinAndSelect('request.requestedBy', 'user')
|
||||||
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
|
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
|
||||||
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
|
||||||
.andWhere('media.mediaType = :mediaType', {
|
.andWhere('media.mediaType = :mediaType', {
|
||||||
mediaType: requestBody.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 (existing && existing.length > 0) {
|
||||||
// If there is an existing movie request that isn't declined, don't allow a new one.
|
// 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()
|
@PrimaryGeneratedColumn()
|
||||||
public id: number;
|
public id: number;
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ export type MediaRequestBody = {
|
|||||||
languageProfileId?: number;
|
languageProfileId?: number;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
tags?: number[];
|
tags?: number[];
|
||||||
|
// Music/Book specific fields
|
||||||
|
foreignId?: string;
|
||||||
|
foreignTitle?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -173,6 +173,16 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
type: MediaType.TV,
|
type: MediaType.TV,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'music':
|
||||||
|
query = query.andWhere('request.type = :type', {
|
||||||
|
type: MediaType.MUSIC,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'book':
|
||||||
|
query = query.andWhere('request.type = :type', {
|
||||||
|
type: MediaType.BOOK,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [requests, requestCount] = await query
|
const [requests, requestCount] = await query
|
||||||
|
|||||||
Reference in New Issue
Block a user