mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { sortBy } from 'lodash';
|
||||
import cacheManager from '../../lib/cache';
|
||||
import ExternalAPI from '../externalapi';
|
||||
import {
|
||||
TmdbCollection,
|
||||
TmdbExternalIdResponse,
|
||||
TmdbLanguage,
|
||||
TmdbMovieDetails,
|
||||
TmdbPersonCombinedCredits,
|
||||
TmdbPersonDetail,
|
||||
TmdbRegion,
|
||||
TmdbSearchMovieResponse,
|
||||
TmdbSearchMultiResponse,
|
||||
TmdbSearchTvResponse,
|
||||
@@ -25,6 +28,8 @@ interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
primaryReleaseDateGte?: string;
|
||||
primaryReleaseDateLte?: string;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
@@ -45,6 +50,9 @@ interface DiscoverMovieOptions {
|
||||
interface DiscoverTvOptions {
|
||||
page?: number;
|
||||
language?: string;
|
||||
firstAirDateGte?: string;
|
||||
firstAirDateLte?: string;
|
||||
includeEmptyReleaseDate?: boolean;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
@@ -57,7 +65,12 @@ interface DiscoverTvOptions {
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
constructor() {
|
||||
private region?: string;
|
||||
private originalLanguage?: string;
|
||||
constructor({
|
||||
region,
|
||||
originalLanguage,
|
||||
}: { region?: string; originalLanguage?: string } = {}) {
|
||||
super(
|
||||
'https://api.themoviedb.org/3',
|
||||
{
|
||||
@@ -67,6 +80,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
}
|
||||
);
|
||||
this.region = region;
|
||||
this.originalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
public searchMulti = async ({
|
||||
@@ -343,6 +358,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
primaryReleaseDateGte,
|
||||
primaryReleaseDateLte,
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
@@ -351,6 +368,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
with_release_type: '3|2',
|
||||
region: this.region,
|
||||
with_original_language: this.originalLanguage,
|
||||
'primary_release_date.gte': primaryReleaseDateGte,
|
||||
'primary_release_date.lte': primaryReleaseDateLte,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -363,7 +385,10 @@ class TheMovieDb extends ExternalAPI {
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en',
|
||||
language = 'en-US',
|
||||
firstAirDateGte,
|
||||
firstAirDateLte,
|
||||
includeEmptyReleaseDate = false,
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
@@ -371,6 +396,11 @@ class TheMovieDb extends ExternalAPI {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
'first_air_date.gte': firstAirDateGte,
|
||||
'first_air_date.lte': firstAirDateLte,
|
||||
with_original_language: this.originalLanguage,
|
||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -394,6 +424,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
originalLanguage: this.originalLanguage,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -420,6 +452,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -594,6 +627,38 @@ class TheMovieDb extends ExternalAPI {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRegions(): Promise<TmdbRegion[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbRegion[]>(
|
||||
'/configuration/countries',
|
||||
{},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const regions = sortBy(data, 'english_name');
|
||||
|
||||
return regions;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getLanguages(): Promise<TmdbLanguage[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbLanguage[]>(
|
||||
'/configuration/languages',
|
||||
{},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const languages = sortBy(data, 'english_name');
|
||||
|
||||
return languages;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
||||
|
@@ -370,3 +370,14 @@ export interface TmdbCollection {
|
||||
backdrop_path?: string;
|
||||
parts: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbRegion {
|
||||
iso_3166_1: string;
|
||||
english_name: string;
|
||||
}
|
||||
|
||||
export interface TmdbLanguage {
|
||||
iso_639_1: string;
|
||||
english_name: string;
|
||||
name: string;
|
||||
}
|
||||
|
@@ -25,4 +25,10 @@ export class UserSettings {
|
||||
|
||||
@Column({ nullable: true })
|
||||
public discordId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public region?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public originalLanguage?: string;
|
||||
}
|
||||
|
@@ -1,3 +1,9 @@
|
||||
export interface UserSettingsGeneralResponse {
|
||||
username?: string;
|
||||
region?: string;
|
||||
originalLanguage?: string;
|
||||
}
|
||||
|
||||
export interface UserSettingsNotificationsResponse {
|
||||
enableNotifications: boolean;
|
||||
discordId?: string;
|
||||
|
@@ -10,6 +10,17 @@ export interface Library {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
iso_3166_1: string;
|
||||
english_name: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
iso_639_1: string;
|
||||
english_name: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PlexSettings {
|
||||
name: string;
|
||||
machineId?: string;
|
||||
@@ -58,6 +69,8 @@ export interface MainSettings {
|
||||
defaultPermissions: number;
|
||||
hideAvailable: boolean;
|
||||
localLogin: boolean;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
trustProxy: boolean;
|
||||
}
|
||||
|
||||
@@ -177,6 +190,8 @@ class Settings {
|
||||
defaultPermissions: Permission.REQUEST,
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
},
|
||||
plex: {
|
||||
|
32
server/migration/1613955393450-UpdateUserSettingsRegions.ts
Normal file
32
server/migration/1613955393450-UpdateUserSettingsRegions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateUserSettingsRegions1613955393450
|
||||
implements MigrationInterface {
|
||||
name = 'UpdateUserSettingsRegions1613955393450';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_settings"("id", "enableNotifications", "discordId", "userId") SELECT "id", "enableNotifications", "discordId", "userId" FROM "user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_settings"("id", "enableNotifications", "discordId", "userId") SELECT "id", "enableNotifications", "discordId", "userId" FROM "temporary_user_settings"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||
}
|
||||
}
|
@@ -4,11 +4,17 @@ import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
|
||||
import Media from '../entity/Media';
|
||||
import { isMovie, isPerson } from '../utils/typeHelpers';
|
||||
import { MediaType } from '../constants/media';
|
||||
import { getSettings } from '../lib/settings';
|
||||
|
||||
const discoverRoutes = Router();
|
||||
|
||||
discoverRoutes.get('/movies', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const settings = getSettings();
|
||||
const tmdb = new TheMovieDb({
|
||||
region: req.user?.settings?.region ?? settings.main.region,
|
||||
originalLanguage:
|
||||
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
|
||||
});
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
@@ -35,11 +41,23 @@ discoverRoutes.get('/movies', async (req, res) => {
|
||||
});
|
||||
|
||||
discoverRoutes.get('/movies/upcoming', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const settings = getSettings();
|
||||
const tmdb = new TheMovieDb({
|
||||
region: req.user?.settings?.region ?? settings.main.region,
|
||||
originalLanguage:
|
||||
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
|
||||
});
|
||||
|
||||
const data = await tmdb.getUpcomingMovies({
|
||||
const now = new Date();
|
||||
const offset = now.getTimezoneOffset();
|
||||
const date = new Date(now.getTime() - offset * 60 * 1000)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
|
||||
const data = await tmdb.getDiscoverMovies({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
primaryReleaseDateGte: date,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
@@ -62,7 +80,12 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
|
||||
});
|
||||
|
||||
discoverRoutes.get('/tv', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const settings = getSettings();
|
||||
const tmdb = new TheMovieDb({
|
||||
region: req.user?.settings?.region ?? settings.main.region,
|
||||
originalLanguage:
|
||||
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
|
||||
});
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
@@ -88,8 +111,52 @@ discoverRoutes.get('/tv', async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
discoverRoutes.get('/tv/upcoming', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
const tmdb = new TheMovieDb({
|
||||
region: req.user?.settings?.region ?? settings.main.region,
|
||||
originalLanguage:
|
||||
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const offset = now.getTimezoneOffset();
|
||||
const date = new Date(now.getTime() - offset * 60 * 1000)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
|
||||
const data = await tmdb.getDiscoverTv({
|
||||
page: Number(req.query.page),
|
||||
language: req.query.language as string,
|
||||
firstAirDateGte: date,
|
||||
});
|
||||
|
||||
const media = await Media.getRelatedMedia(
|
||||
data.results.map((result) => result.id)
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
page: data.page,
|
||||
totalPages: data.total_pages,
|
||||
totalResults: data.total_results,
|
||||
results: data.results.map((result) =>
|
||||
mapTvResult(
|
||||
result,
|
||||
media.find(
|
||||
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
discoverRoutes.get('/trending', async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
const settings = getSettings();
|
||||
const tmdb = new TheMovieDb({
|
||||
region: req.user?.settings?.region ?? settings.main.region,
|
||||
originalLanguage:
|
||||
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
|
||||
});
|
||||
|
||||
const data = await tmdb.getAllTrending({
|
||||
page: Number(req.query.page),
|
||||
|
@@ -16,6 +16,7 @@ import collectionRoutes from './collection';
|
||||
import { getAppVersion, getCommitTag } from '../utils/appVersion';
|
||||
import serviceRoutes from './service';
|
||||
import { appDataStatus, appDataPath } from '../utils/appDataVolume';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -57,6 +58,22 @@ router.use('/collection', isAuthenticated(), collectionRoutes);
|
||||
router.use('/service', isAuthenticated(), serviceRoutes);
|
||||
router.use('/auth', authRoutes);
|
||||
|
||||
router.get('/regions', isAuthenticated(), async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const regions = await tmdb.getRegions();
|
||||
|
||||
return res.status(200).json(regions);
|
||||
});
|
||||
|
||||
router.get('/languages', isAuthenticated(), async (req, res) => {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
const languages = await tmdb.getLanguages();
|
||||
|
||||
return res.status(200).json(languages);
|
||||
});
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
return res.status(200).json({
|
||||
api: 'Overseerr API',
|
||||
|
@@ -2,7 +2,10 @@ import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../../entity/User';
|
||||
import { UserSettings } from '../../entity/UserSettings';
|
||||
import { UserSettingsNotificationsResponse } from '../../interfaces/api/userSettingsInterfaces';
|
||||
import {
|
||||
UserSettingsGeneralResponse,
|
||||
UserSettingsNotificationsResponse,
|
||||
} from '../../interfaces/api/userSettingsInterfaces';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
@@ -25,7 +28,7 @@ const isOwnProfileOrAdmin = (): Middleware => {
|
||||
|
||||
const userSettingsRoutes = Router({ mergeParams: true });
|
||||
|
||||
userSettingsRoutes.get<{ id: string }, { username?: string }>(
|
||||
userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
'/main',
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
@@ -40,7 +43,11 @@ userSettingsRoutes.get<{ id: string }, { username?: string }>(
|
||||
return next({ status: 404, message: 'User not found.' });
|
||||
}
|
||||
|
||||
return res.status(200).json({ username: user.username });
|
||||
return res.status(200).json({
|
||||
username: user.username,
|
||||
region: user.settings?.region,
|
||||
originalLanguage: user.settings?.originalLanguage,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
}
|
||||
@@ -49,8 +56,8 @@ userSettingsRoutes.get<{ id: string }, { username?: string }>(
|
||||
|
||||
userSettingsRoutes.post<
|
||||
{ id: string },
|
||||
{ username?: string },
|
||||
{ username?: string }
|
||||
UserSettingsGeneralResponse,
|
||||
UserSettingsGeneralResponse
|
||||
>('/main', isOwnProfileOrAdmin(), async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
@@ -64,6 +71,16 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
if (!user.settings) {
|
||||
user.settings = new UserSettings({
|
||||
user: req.user,
|
||||
region: req.body.region,
|
||||
originalLanguage: req.body.originalLanguage,
|
||||
});
|
||||
} else {
|
||||
user.settings.region = req.body.region;
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
|
||||
|
Reference in New Issue
Block a user