mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
Merge branch 'develop' into features/deleteMediaFile
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import JellyfinAPI from '../api/jellyfin';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
import { UserType } from '../constants/user';
|
||||
import { User } from '../entity/User';
|
||||
import { startJobs } from '../job/schedule';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import { startJobs } from '@server/job/schedule';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -89,8 +89,8 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken', 'plexId'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true, plexId: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
@@ -424,8 +424,8 @@ authRoutes.post('/local', async (req, res, next) => {
|
||||
}
|
||||
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken', 'plexId'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true, plexId: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import Media from '@server/entity/Media';
|
||||
import logger from '@server/logger';
|
||||
import { mapCollection } from '@server/models/Collection';
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import { mapCollection } from '../models/Collection';
|
||||
|
||||
const collectionRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { User } from '@server/entity/User';
|
||||
import type {
|
||||
GenreSliderItem,
|
||||
WatchlistResponse,
|
||||
} from '@server/interfaces/api/discoverInterfaces';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { mapProductionCompany } from '@server/models/Movie';
|
||||
import {
|
||||
mapMovieResult,
|
||||
mapPersonResult,
|
||||
mapTvResult,
|
||||
} from '@server/models/Search';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import { isMovie, isPerson } from '@server/utils/typeHelpers';
|
||||
import { Router } from 'express';
|
||||
import { sortBy } from 'lodash';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { User } from '../entity/User';
|
||||
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { mapProductionCompany } from '../models/Movie';
|
||||
import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search';
|
||||
import { mapNetwork } from '../models/Tv';
|
||||
import { isMovie, isPerson } from '../utils/typeHelpers';
|
||||
|
||||
export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
||||
const settings = getSettings();
|
||||
@@ -704,4 +713,45 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
||||
}
|
||||
);
|
||||
|
||||
discoverRoutes.get<{ page?: number }, WatchlistResponse>(
|
||||
'/watchlist',
|
||||
async (req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
const itemsPerPage = 20;
|
||||
const page = req.params.page ?? 1;
|
||||
const offset = (page - 1) * itemsPerPage;
|
||||
|
||||
const activeUser = await userRepository.findOne({
|
||||
where: { id: req.user?.id },
|
||||
select: ['id', 'plexToken'],
|
||||
});
|
||||
|
||||
if (!activeUser?.plexToken) {
|
||||
// We will just return an empty array if the user has no Plex token
|
||||
return res.json({
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
totalResults: 0,
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
|
||||
const plexTV = new PlexTvAPI(activeUser.plexToken);
|
||||
|
||||
const watchlist = await plexTV.getWatchlist({ offset });
|
||||
|
||||
return res.json({
|
||||
page,
|
||||
totalPages: Math.ceil(watchlist.size / itemsPerPage),
|
||||
totalResults: watchlist.size,
|
||||
results: watchlist.items.map((item) => ({
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
mediaType: item.type === 'show' ? 'tv' : 'movie',
|
||||
tmdbId: item.tmdbId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default discoverRoutes;
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import GithubAPI from '@server/api/github';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import type {
|
||||
TmdbMovieResult,
|
||||
TmdbTvResult,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { checkUser, isAuthenticated } from '@server/middleware/auth';
|
||||
import { mapProductionCompany } from '@server/models/Movie';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import settingsRoutes from '@server/routes/settings';
|
||||
import { appDataPath, appDataStatus } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion, getCommitTag } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { isPerson } from '@server/utils/typeHelpers';
|
||||
import { Router } from 'express';
|
||||
import GithubAPI from '../api/github';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { TmdbMovieResult, TmdbTvResult } from '../api/themoviedb/interfaces';
|
||||
import { StatusResponse } from '../interfaces/api/settingsInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { checkUser, isAuthenticated } from '../middleware/auth';
|
||||
import { mapProductionCompany } from '../models/Movie';
|
||||
import { mapNetwork } from '../models/Tv';
|
||||
import { appDataPath, appDataStatus } from '../utils/appDataVolume';
|
||||
import { getAppVersion, getCommitTag } from '../utils/appVersion';
|
||||
import { isPerson } from '../utils/typeHelpers';
|
||||
import authRoutes from './auth';
|
||||
import collectionRoutes from './collection';
|
||||
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
|
||||
@@ -23,7 +28,6 @@ import personRoutes from './person';
|
||||
import requestRoutes from './request';
|
||||
import searchRoutes from './search';
|
||||
import serviceRoutes from './service';
|
||||
import settingsRoutes from './settings';
|
||||
import tvRoutes from './tv';
|
||||
import user from './user';
|
||||
|
||||
@@ -75,6 +79,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
|
||||
commitTag: getCommitTag(),
|
||||
updateAvailable,
|
||||
commitsBehind,
|
||||
restartRequired: restartFlag.isSet(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,11 +102,7 @@ router.get('/settings/public', async (req, res) => {
|
||||
return res.status(200).json(settings.fullPublicSettings);
|
||||
}
|
||||
});
|
||||
router.use(
|
||||
'/settings',
|
||||
isAuthenticated(Permission.MANAGE_SETTINGS),
|
||||
settingsRoutes
|
||||
);
|
||||
router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes);
|
||||
router.use('/search', isAuthenticated(), searchRoutes);
|
||||
router.use('/discover', isAuthenticated(), discoverRoutes);
|
||||
router.use('/request', isAuthenticated(), requestRoutes);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { IssueStatus, IssueType } from '@server/constants/issue';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Issue from '@server/entity/Issue';
|
||||
import IssueComment from '@server/entity/IssueComment';
|
||||
import Media from '@server/entity/Media';
|
||||
import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { IssueStatus, IssueType } from '../constants/issue';
|
||||
import Issue from '../entity/Issue';
|
||||
import IssueComment from '../entity/IssueComment';
|
||||
import Media from '../entity/Media';
|
||||
import { IssueResultsResponse } from '../interfaces/api/issueInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const issueRoutes = Router();
|
||||
|
||||
@@ -365,7 +365,7 @@ issueRoutes.delete(
|
||||
try {
|
||||
const issue = await issueRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.issueId) },
|
||||
relations: ['createdBy'],
|
||||
relations: { createdBy: true },
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getRepository } from '@server/datasource';
|
||||
import IssueComment from '@server/entity/IssueComment';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import IssueComment from '../entity/IssueComment';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const issueCommentRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Router } from 'express';
|
||||
import { FindOneOptions, FindOperator, getRepository, In } from 'typeorm';
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import TautulliAPI from '../api/tautulli';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { User } from '../entity/User';
|
||||
import {
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { User } from '@server/entity/User';
|
||||
import type {
|
||||
MediaResultsResponse,
|
||||
MediaWatchDataResponse,
|
||||
} from '../interfaces/api/mediaInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
} from '@server/interfaces/api/mediaInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
|
||||
|
||||
const mediaRoutes = Router();
|
||||
|
||||
@@ -24,8 +26,7 @@ mediaRoutes.get('/', async (req, res, next) => {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 20;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
|
||||
let statusFilter: MediaStatus | FindOperator<MediaStatus> | undefined =
|
||||
undefined;
|
||||
let statusFilter = undefined;
|
||||
|
||||
switch (req.query.filter) {
|
||||
case 'available':
|
||||
@@ -69,7 +70,7 @@ mediaRoutes.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const [media, mediaCount] = await mediaRepository.findAndCount({
|
||||
order: sortFilter,
|
||||
where: {
|
||||
where: statusFilter && {
|
||||
status: statusFilter,
|
||||
},
|
||||
take: pageSize,
|
||||
@@ -154,7 +155,7 @@ mediaRoutes.delete(
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
const media = await mediaRepository.findOneOrFail({
|
||||
where: { id: req.params.id },
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
await mediaRepository.remove(media);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import RottenTomatoes from '@server/api/rottentomatoes';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import Media from '@server/entity/Media';
|
||||
import logger from '@server/logger';
|
||||
import { mapMovieDetails } from '@server/models/Movie';
|
||||
import { mapMovieResult } from '@server/models/Search';
|
||||
import { Router } from 'express';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import { mapMovieDetails } from '../models/Movie';
|
||||
import { mapMovieResult } from '../models/Search';
|
||||
|
||||
const movieRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import Media from '@server/entity/Media';
|
||||
import logger from '@server/logger';
|
||||
import {
|
||||
mapCastCredits,
|
||||
mapCrewCredits,
|
||||
mapPersonDetails,
|
||||
} from '../models/Person';
|
||||
} from '@server/models/Person';
|
||||
import { Router } from 'express';
|
||||
|
||||
const personRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
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';
|
||||
import { getRepository } from 'typeorm';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import SeasonRequest from '../entity/SeasonRequest';
|
||||
import { User } from '../entity/User';
|
||||
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const requestRoutes = Router();
|
||||
|
||||
@@ -40,11 +52,15 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
MediaRequestStatus.APPROVED,
|
||||
];
|
||||
break;
|
||||
case 'failed':
|
||||
statusFilter = [MediaRequestStatus.FAILED];
|
||||
break;
|
||||
default:
|
||||
statusFilter = [
|
||||
MediaRequestStatus.PENDING,
|
||||
MediaRequestStatus.APPROVED,
|
||||
MediaRequestStatus.DECLINED,
|
||||
MediaRequestStatus.FAILED,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -142,302 +158,38 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
}
|
||||
);
|
||||
|
||||
requestRoutes.post('/', async (req, res, next) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
try {
|
||||
let requestUser = req.user;
|
||||
|
||||
if (
|
||||
req.body.userId &&
|
||||
!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 (!requestUser) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User missing from request context.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
req.body.mediaType === MediaType.MOVIE &&
|
||||
!req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
|
||||
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: `You do not have permission to make ${
|
||||
req.body.is4k ? '4K ' : ''
|
||||
}movie requests.`,
|
||||
});
|
||||
} else if (
|
||||
req.body.mediaType === MediaType.TV &&
|
||||
!req.user?.hasPermission(
|
||||
req.body.is4k
|
||||
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
|
||||
: [Permission.REQUEST, Permission.REQUEST_TV],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: `You do not have permission to make ${
|
||||
req.body.is4k ? '4K ' : ''
|
||||
}series requests.`,
|
||||
});
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Movie Quota Exceeded',
|
||||
});
|
||||
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
req.body.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: req.body.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||
|
||||
let media = await mediaRepository.findOne({
|
||||
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: tmdbMedia.id,
|
||||
tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||
mediaType: req.body.mediaType,
|
||||
});
|
||||
} else {
|
||||
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
|
||||
media.status = MediaStatus.PENDING;
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
|
||||
media.status4k = MediaStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
const existing = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.media', 'media')
|
||||
.where('request.is4k = :is4k', { is4k: req.body.is4k })
|
||||
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
||||
.andWhere('media.mediaType = :mediaType', {
|
||||
mediaType: MediaType.MOVIE,
|
||||
})
|
||||
.andWhere('request.status != :requestStatus', {
|
||||
requestStatus: MediaRequestStatus.DECLINED,
|
||||
})
|
||||
.getOne();
|
||||
|
||||
if (existing) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
mediaType: req.body.mediaType,
|
||||
is4k: req.body.is4k,
|
||||
label: 'Media Request',
|
||||
});
|
||||
requestRoutes.post<never, MediaRequest, MediaRequestBody>(
|
||||
'/',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 409,
|
||||
message: 'Request for this media already exists.',
|
||||
status: 401,
|
||||
message: 'You must be logged in to request media.',
|
||||
});
|
||||
}
|
||||
const request = await MediaRequest.request(req.body, req.user);
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.MOVIE,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||
: Permission.AUTO_APPROVE_MOVIE,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
tags: req.body.tags,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const requestedSeasons = req.body.seasons as number[];
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
|
||||
// (Unless there are no seasons, in which case we abort)
|
||||
if (media.requests) {
|
||||
existingSeasons = media.requests
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === req.body.is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
)
|
||||
.reduce((seasons, request) => {
|
||||
const combinedSeasons = request.seasons.map(
|
||||
(season) => season.seasonNumber
|
||||
);
|
||||
|
||||
return [...seasons, ...combinedSeasons];
|
||||
}, [] as number[]);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalSeasons = requestedSeasons.filter(
|
||||
(rs) => !existingSeasons.includes(rs)
|
||||
);
|
||||
|
||||
if (finalSeasons.length === 0) {
|
||||
return next({
|
||||
status: 202,
|
||||
message: 'No seasons available to request',
|
||||
});
|
||||
} else if (
|
||||
quotas.tv.limit &&
|
||||
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
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 });
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
const request = new MediaRequest({
|
||||
type: MediaType.TV,
|
||||
media,
|
||||
requestedBy: requestUser,
|
||||
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
modifiedBy: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? req.user
|
||||
: undefined,
|
||||
is4k: req.body.is4k,
|
||||
serverId: req.body.serverId,
|
||||
profileId: req.body.profileId,
|
||||
rootFolder: req.body.rootFolder,
|
||||
languageProfileId: req.body.languageProfileId,
|
||||
tags: req.body.tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
seasonNumber: sn,
|
||||
status: req.user?.hasPermission(
|
||||
[
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K
|
||||
: Permission.AUTO_APPROVE,
|
||||
req.body.is4k
|
||||
? Permission.AUTO_APPROVE_4K_TV
|
||||
: Permission.AUTO_APPROVE_TV,
|
||||
Permission.MANAGE_REQUESTS,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
? MediaRequestStatus.APPROVED
|
||||
: MediaRequestStatus.PENDING,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
}
|
||||
|
||||
next({ status: 500, message: 'Invalid media type' });
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
requestRoutes.get('/count', async (_req, res, next) => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
@@ -528,7 +280,7 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
|
||||
try {
|
||||
const request = await requestRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.requestId) },
|
||||
relations: ['requestedBy', 'modifiedBy'],
|
||||
relations: { requestedBy: true, modifiedBy: true },
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -560,9 +312,11 @@ requestRoutes.put<{ requestId: string }>(
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
try {
|
||||
const request = await requestRepository.findOne(
|
||||
Number(req.params.requestId)
|
||||
);
|
||||
const request = await requestRepository.findOne({
|
||||
where: {
|
||||
id: Number(req.params.requestId),
|
||||
},
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
return next({ status: 404, message: 'Request not found.' });
|
||||
@@ -628,7 +382,7 @@ requestRoutes.put<{ requestId: string }>(
|
||||
// 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'],
|
||||
relations: { requests: true },
|
||||
});
|
||||
|
||||
// Get all requested seasons that are not part of this request we are editing
|
||||
@@ -698,7 +452,7 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
|
||||
try {
|
||||
const request = await requestRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.requestId) },
|
||||
relations: ['requestedBy', 'modifiedBy'],
|
||||
relations: { requestedBy: true, modifiedBy: true },
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -735,7 +489,7 @@ requestRoutes.post<{
|
||||
try {
|
||||
const request = await requestRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.requestId) },
|
||||
relations: ['requestedBy', 'modifiedBy'],
|
||||
relations: { requestedBy: true, modifiedBy: true },
|
||||
});
|
||||
|
||||
await request.updateParentStatus();
|
||||
@@ -763,7 +517,7 @@ requestRoutes.post<{
|
||||
try {
|
||||
const request = await requestRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.requestId) },
|
||||
relations: ['requestedBy', 'modifiedBy'],
|
||||
relations: { requestedBy: true, modifiedBy: true },
|
||||
});
|
||||
|
||||
let newStatus: MediaRequestStatus;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces';
|
||||
import Media from '@server/entity/Media';
|
||||
import { findSearchProvider } from '@server/lib/search';
|
||||
import logger from '@server/logger';
|
||||
import { mapSearchResults } from '@server/models/Search';
|
||||
import { Router } from 'express';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { TmdbSearchMultiResponse } from '../api/themoviedb/interfaces';
|
||||
import Media from '../entity/Media';
|
||||
import { findSearchProvider } from '../lib/search';
|
||||
import logger from '../logger';
|
||||
import { mapSearchResults } from '../models/Search';
|
||||
|
||||
const searchRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import RadarrAPI from '../api/servarr/radarr';
|
||||
import SonarrAPI from '../api/servarr/sonarr';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import {
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import type {
|
||||
ServiceCommonServer,
|
||||
ServiceCommonServerWithDetails,
|
||||
} from '../interfaces/api/serviceInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
} from '@server/interfaces/api/serviceInterfaces';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
|
||||
const serviceRoutes = Router();
|
||||
|
||||
@@ -191,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>(
|
||||
try {
|
||||
const tv = await tmdb.getTvShow({
|
||||
tvId: Number(req.params.tmdbId),
|
||||
language: req.locale ?? (req.query.language as string),
|
||||
language: 'en',
|
||||
});
|
||||
|
||||
const response = await sonarr.getSeriesByTitle(tv.name);
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import { merge, omit, set, sortBy } from 'lodash';
|
||||
import { rescheduleJob } from 'node-schedule';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { URL } from 'url';
|
||||
import JellyfinAPI from '../../api/jellyfin';
|
||||
import PlexAPI from '../../api/plexapi';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import TautulliAPI from '../../api/tautulli';
|
||||
import Media from '../../entity/Media';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { User } from '../../entity/User';
|
||||
import { PlexConnection } from '../../interfaces/api/plexInterfaces';
|
||||
import {
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { PlexConnection } from '@server/interfaces/api/plexInterfaces';
|
||||
import type {
|
||||
LogMessage,
|
||||
LogsResultsResponse,
|
||||
SettingsAboutResponse,
|
||||
} from '../../interfaces/api/settingsInterfaces';
|
||||
import { jobJellyfinFullSync } from '../../job/jellyfinsync';
|
||||
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 { appDataPath } from '../../utils/appDataVolume';
|
||||
import { getAppVersion } from '../../utils/appVersion';
|
||||
} from '@server/interfaces/api/settingsInterfaces';
|
||||
import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
|
||||
import { scheduledJobs } from '@server/job/schedule';
|
||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { plexFullScanner } from '@server/lib/scanners/plex';
|
||||
import type { Library, MainSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { appDataPath } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
|
||||
import { rescheduleJob } from 'node-schedule';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import { URL } from 'url';
|
||||
import notificationRoutes from './notifications';
|
||||
import radarrRoutes from './radarr';
|
||||
import sonarrRoutes from './sonarr';
|
||||
@@ -93,8 +95,8 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
||||
const settings = getSettings();
|
||||
try {
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
Object.assign(settings.plex, req.body);
|
||||
@@ -129,8 +131,8 @@ 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' },
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const plexTvClient = admin.plexToken
|
||||
? new PlexTvAPI(admin.plexToken)
|
||||
@@ -208,8 +210,8 @@ settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
if (req.query.sync) {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
||||
|
||||
@@ -262,6 +264,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||
where: { id: 1 },
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
@@ -312,6 +315,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||
where: { id: 1 },
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
@@ -390,8 +394,8 @@ settingsRoutes.get(
|
||||
|
||||
try {
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const plexApi = new PlexTvAPI(admin.plexToken ?? '');
|
||||
const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map(
|
||||
@@ -450,6 +454,8 @@ settingsRoutes.get(
|
||||
(req, res, next) => {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 25;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
const search = (req.query.search as string) ?? '';
|
||||
const searchRegexp = new RegExp(escapeRegExp(search), 'i');
|
||||
|
||||
let filter: string[] = [];
|
||||
switch (req.query.filter) {
|
||||
@@ -481,6 +487,22 @@ settingsRoutes.get(
|
||||
'data',
|
||||
];
|
||||
|
||||
const deepValueStrings = (obj: Record<string, unknown>): string[] => {
|
||||
const values = [];
|
||||
|
||||
for (const val of Object.values(obj)) {
|
||||
if (typeof val === 'string') {
|
||||
values.push(val);
|
||||
} else if (typeof val === 'number') {
|
||||
values.push(val.toString());
|
||||
} else if (val !== null && typeof val === 'object') {
|
||||
values.push(...deepValueStrings(val as Record<string, unknown>));
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
try {
|
||||
fs.readFileSync(logFile, 'utf-8')
|
||||
.split('\n')
|
||||
@@ -505,6 +527,19 @@ settingsRoutes.get(
|
||||
});
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
if (
|
||||
// label and data are sometimes undefined
|
||||
!searchRegexp.test(logMessage.label ?? '') &&
|
||||
!searchRegexp.test(logMessage.message) &&
|
||||
!deepValueStrings(logMessage.data ?? {}).some((val) =>
|
||||
searchRegexp.test(val)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logs.push(logMessage);
|
||||
});
|
||||
|
||||
@@ -539,6 +574,7 @@ settingsRoutes.get('/jobs', (_req, res) => {
|
||||
name: job.name,
|
||||
type: job.type,
|
||||
interval: job.interval,
|
||||
cronSchedule: job.cronSchedule,
|
||||
nextExecutionTime: job.job.nextInvocation(),
|
||||
running: job.running ? job.running() : false,
|
||||
}))
|
||||
@@ -559,6 +595,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
|
||||
name: scheduledJob.name,
|
||||
type: scheduledJob.type,
|
||||
interval: scheduledJob.interval,
|
||||
cronSchedule: scheduledJob.cronSchedule,
|
||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||
});
|
||||
@@ -584,6 +621,7 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
name: scheduledJob.name,
|
||||
type: scheduledJob.type,
|
||||
interval: scheduledJob.interval,
|
||||
cronSchedule: scheduledJob.cronSchedule,
|
||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||
});
|
||||
@@ -608,11 +646,14 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||
settings.save();
|
||||
|
||||
scheduledJob.cronSchedule = req.body.schedule;
|
||||
|
||||
return res.status(200).json({
|
||||
id: scheduledJob.id,
|
||||
name: scheduledJob.name,
|
||||
type: scheduledJob.type,
|
||||
interval: scheduledJob.interval,
|
||||
cronSchedule: scheduledJob.cronSchedule,
|
||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||
});
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import type { User } from '@server/entity/User';
|
||||
import { Notification } from '@server/lib/notifications';
|
||||
import type { NotificationAgent } from '@server/lib/notifications/agents/agent';
|
||||
import DiscordAgent from '@server/lib/notifications/agents/discord';
|
||||
import EmailAgent from '@server/lib/notifications/agents/email';
|
||||
import GotifyAgent from '@server/lib/notifications/agents/gotify';
|
||||
import LunaSeaAgent from '@server/lib/notifications/agents/lunasea';
|
||||
import PushbulletAgent from '@server/lib/notifications/agents/pushbullet';
|
||||
import PushoverAgent from '@server/lib/notifications/agents/pushover';
|
||||
import SlackAgent from '@server/lib/notifications/agents/slack';
|
||||
import TelegramAgent from '@server/lib/notifications/agents/telegram';
|
||||
import WebhookAgent from '@server/lib/notifications/agents/webhook';
|
||||
import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { Router } from 'express';
|
||||
import { User } from '../../entity/User';
|
||||
import { Notification } from '../../lib/notifications';
|
||||
import { NotificationAgent } from '../../lib/notifications/agents/agent';
|
||||
import DiscordAgent from '../../lib/notifications/agents/discord';
|
||||
import EmailAgent from '../../lib/notifications/agents/email';
|
||||
import GotifyAgent from '../../lib/notifications/agents/gotify';
|
||||
import LunaSeaAgent from '../../lib/notifications/agents/lunasea';
|
||||
import PushbulletAgent from '../../lib/notifications/agents/pushbullet';
|
||||
import PushoverAgent from '../../lib/notifications/agents/pushover';
|
||||
import SlackAgent from '../../lib/notifications/agents/slack';
|
||||
import TelegramAgent from '../../lib/notifications/agents/telegram';
|
||||
import WebhookAgent from '../../lib/notifications/agents/webhook';
|
||||
import WebPushAgent from '../../lib/notifications/agents/webpush';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
|
||||
const notificationRoutes = Router();
|
||||
|
||||
const sendTestNotification = async (agent: NotificationAgent, user: User) =>
|
||||
await agent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifySystem: true,
|
||||
notifyAdmin: false,
|
||||
notifyUser: user,
|
||||
subject: 'Test Notification',
|
||||
@@ -247,7 +248,7 @@ notificationRoutes.post('/webpush/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -363,7 +364,7 @@ notificationRoutes.post('/lunasea/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -384,34 +385,26 @@ notificationRoutes.get('/gotify', (_req, res) => {
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/gotify', (req, rest) => {
|
||||
notificationRoutes.post('/gotify', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
settings.notifications.agents.gotify = req.body;
|
||||
settings.save();
|
||||
|
||||
rest.status(200).json(settings.notifications.agents.gotify);
|
||||
res.status(200).json(settings.notifications.agents.gotify);
|
||||
});
|
||||
|
||||
notificationRoutes.post('/gotify/test', async (req, rest, next) => {
|
||||
notificationRoutes.post('/gotify/test', async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User information is missing from request',
|
||||
message: 'User information is missing from the request.',
|
||||
});
|
||||
}
|
||||
|
||||
const gotifyAgent = new GotifyAgent(req.body);
|
||||
if (
|
||||
await gotifyAgent.send(Notification.TEST_NOTIFICATION, {
|
||||
notifyAdmin: false,
|
||||
notifyUser: req.user,
|
||||
subject: 'Test Notification',
|
||||
message:
|
||||
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
|
||||
})
|
||||
) {
|
||||
return rest.status(204).send();
|
||||
if (await sendTestNotification(gotifyAgent, req.user)) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return next({
|
||||
status: 500,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import type { RadarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
import RadarrAPI from '../../api/servarr/radarr';
|
||||
import { getSettings, RadarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
const radarrRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import type { SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
import SonarrAPI from '../../api/servarr/sonarr';
|
||||
import { getSettings, SonarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
const sonarrRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import RottenTomatoes from '@server/api/rottentomatoes';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import Media from '@server/entity/Media';
|
||||
import logger from '@server/logger';
|
||||
import { mapTvResult } from '@server/models/Search';
|
||||
import { mapSeasonWithEpisodes, mapTvDetails } from '@server/models/Tv';
|
||||
import { Router } from 'express';
|
||||
import RottenTomatoes from '../api/rottentomatoes';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import logger from '../logger';
|
||||
import { mapTvResult } from '../models/Search';
|
||||
import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv';
|
||||
|
||||
const tvRoutes = Router();
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { findIndex, sortBy } from 'lodash';
|
||||
import { getRepository, In, Not } from 'typeorm';
|
||||
import JellyfinAPI from '../../api/jellyfin';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import TautulliAPI from '../../api/tautulli';
|
||||
import { MediaType } from '../../constants/media';
|
||||
import { UserType } from '../../constants/user';
|
||||
import Media from '../../entity/Media';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { User } from '../../entity/User';
|
||||
import { UserPushSubscription } from '../../entity/UserPushSubscription';
|
||||
import {
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import { User } from '@server/entity/User';
|
||||
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||
import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces';
|
||||
import type {
|
||||
QuotaResponse,
|
||||
UserRequestsResponse,
|
||||
UserResultsResponse,
|
||||
UserWatchDataResponse,
|
||||
} from '../../interfaces/api/userInterfaces';
|
||||
import { hasPermission, Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
} from '@server/interfaces/api/userInterfaces';
|
||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { findIndex, sortBy } from 'lodash';
|
||||
import { In } from 'typeorm';
|
||||
import userSettingsRoutes from './usersettings';
|
||||
|
||||
const router = Router();
|
||||
@@ -259,12 +261,7 @@ export const canMakePermissionsChange = (
|
||||
user?: User
|
||||
): boolean =>
|
||||
// Only let the owner grant admin privileges
|
||||
!(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1) ||
|
||||
// Only let users with the manage settings permission, grant the same permission
|
||||
!(
|
||||
hasPermission(Permission.MANAGE_SETTINGS, permissions) &&
|
||||
!hasPermission(Permission.MANAGE_SETTINGS, user?.permissions ?? 0)
|
||||
);
|
||||
!(hasPermission(Permission.ADMIN, permissions) && user?.id !== 1);
|
||||
|
||||
router.put<
|
||||
Record<string, never>,
|
||||
@@ -283,8 +280,12 @@ router.put<
|
||||
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const users = await userRepository.findByIds(req.body.ids, {
|
||||
...(!isOwner ? { id: Not(1) } : {}),
|
||||
const users: User[] = await userRepository.find({
|
||||
where: {
|
||||
id: In(
|
||||
isOwner ? req.body.ids : req.body.ids.filter((id) => Number(id) !== 1)
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedUsers = await Promise.all(
|
||||
@@ -351,7 +352,7 @@ router.delete<{ id: string }>(
|
||||
|
||||
const user = await userRepository.findOne({
|
||||
where: { id: Number(req.params.id) },
|
||||
relations: ['requests'],
|
||||
relations: { requests: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -410,8 +411,8 @@ router.post(
|
||||
|
||||
// taken from auth.ts
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
select: { id: true, plexToken: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
@@ -477,6 +478,7 @@ router.post(
|
||||
|
||||
// taken from auth.ts
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
where: { id: 1 },
|
||||
select: [
|
||||
'id',
|
||||
'jellyfinAuthToken',
|
||||
@@ -598,7 +600,7 @@ router.get<{ id: string }, UserWatchDataResponse>(
|
||||
try {
|
||||
const user = await getRepository(User).findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
select: ['id', 'plexId'],
|
||||
select: { id: true, plexId: true },
|
||||
});
|
||||
|
||||
const tautulli = new TautulliAPI(settings);
|
||||
@@ -680,4 +682,60 @@ router.get<{ id: string }, UserWatchDataResponse>(
|
||||
}
|
||||
);
|
||||
|
||||
router.get<{ id: string; page?: number }, WatchlistResponse>(
|
||||
'/:id/watchlist',
|
||||
async (req, res, next) => {
|
||||
if (
|
||||
Number(req.params.id) !== req.user?.id &&
|
||||
!req.user?.hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message:
|
||||
"You do not have permission to view this user's Plex Watchlist.",
|
||||
});
|
||||
}
|
||||
|
||||
const itemsPerPage = 20;
|
||||
const page = req.params.page ?? 1;
|
||||
const offset = (page - 1) * itemsPerPage;
|
||||
|
||||
const user = await getRepository(User).findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
select: { id: true, plexToken: true },
|
||||
});
|
||||
|
||||
if (!user?.plexToken) {
|
||||
// We will just return an empty array if the user has no Plex token
|
||||
return res.json({
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
totalResults: 0,
|
||||
results: [],
|
||||
});
|
||||
}
|
||||
|
||||
const plexTV = new PlexTvAPI(user.plexToken);
|
||||
|
||||
const watchlist = await plexTV.getWatchlist({ offset });
|
||||
|
||||
return res.json({
|
||||
page,
|
||||
totalPages: Math.ceil(watchlist.size / itemsPerPage),
|
||||
totalResults: watchlist.size,
|
||||
results: watchlist.items.map((item) => ({
|
||||
ratingKey: item.ratingKey,
|
||||
title: item.title,
|
||||
mediaType: item.type === 'show' ? 'tv' : 'movie',
|
||||
tmdbId: item.tmdbId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { canMakePermissionsChange } from '.';
|
||||
import { User } from '../../entity/User';
|
||||
import { UserSettings } from '../../entity/UserSettings';
|
||||
import {
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import { UserSettings } from '@server/entity/UserSettings';
|
||||
import type {
|
||||
UserSettingsGeneralResponse,
|
||||
UserSettingsNotificationsResponse,
|
||||
} from '../../interfaces/api/userSettingsInterfaces';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
} from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { Router } from 'express';
|
||||
import { canMakePermissionsChange } from '.';
|
||||
|
||||
const isOwnProfileOrAdmin = (): Middleware => {
|
||||
const authMiddleware: Middleware = (req, res, next) => {
|
||||
@@ -64,6 +64,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit,
|
||||
globalTvQuotaDays: defaultQuotas.tv.quotaDays,
|
||||
globalTvQuotaLimit: defaultQuotas.tv.quotaLimit,
|
||||
watchlistSyncMovies: user.settings?.watchlistSyncMovies,
|
||||
watchlistSyncTv: user.settings?.watchlistSyncTv,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -115,12 +117,16 @@ userSettingsRoutes.post<
|
||||
locale: req.body.locale,
|
||||
region: req.body.region,
|
||||
originalLanguage: req.body.originalLanguage,
|
||||
watchlistSyncMovies: req.body.watchlistSyncMovies,
|
||||
watchlistSyncTv: req.body.watchlistSyncTv,
|
||||
});
|
||||
} else {
|
||||
user.settings.discordId = req.body.discordId;
|
||||
user.settings.locale = req.body.locale;
|
||||
user.settings.region = req.body.region;
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
||||
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
||||
user.email = req.body.email ?? user.email;
|
||||
}
|
||||
|
||||
@@ -132,6 +138,8 @@ userSettingsRoutes.post<
|
||||
locale: user.settings.locale,
|
||||
region: user.settings.region,
|
||||
originalLanguage: user.settings.originalLanguage,
|
||||
watchlistSyncMovies: user.settings.watchlistSyncMovies,
|
||||
watchlistSyncTv: user.settings.watchlistSyncTv,
|
||||
email: user.email,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user