Compare commits

..

96 Commits

Author SHA1 Message Date
semantic-release-bot
4a5421a9ba chore(release): 1.30.0 2022-09-19 07:17:15 +00:00
Ryan Cohen
24b9c00b2a Merge branch 'develop' 2022-09-19 16:12:44 +09:00
semantic-release-bot
5125abdbf0 chore(release): 1.29.1 2022-04-06 23:34:24 +00:00
Ryan Cohen
724f8bb538 Merge branch 'develop' 2022-04-07 08:33:04 +09:00
semantic-release-bot
512f6e51e4 chore(release): 1.29.0 2022-04-01 09:03:58 +00:00
Ryan Cohen
1b36dded3f Merge branch 'develop' 2022-04-01 18:02:32 +09:00
Ryan Cohen
77511bf199 ci: disable husky for release ci 2022-04-01 18:01:22 +09:00
Ryan Cohen
26d77af3a4 Merge branch 'develop' 2022-04-01 17:45:51 +09:00
semantic-release-bot
2b111f1c34 chore(release): 1.28.0 2022-01-01 04:20:46 +00:00
Ryan Cohen
a6392cb2db Merge branch 'develop' 2022-01-01 13:11:57 +09:00
semantic-release-bot
6d55b50e5e chore(release): 1.27.0 2021-10-19 14:01:17 +00:00
sct
f7dc6256ba Merge branch 'develop' 2021-10-19 22:54:07 +09:00
semantic-release-bot
f1df1caea5 chore(release): 1.26.1 2021-09-20 00:22:06 +00:00
sct
d23c120dc0 Merge branch 'develop' 2021-09-20 09:14:29 +09:00
semantic-release-bot
353efa39d0 chore(release): 1.26.0 2021-09-19 12:15:52 +00:00
sct
21cdf7d113 Merge branch 'develop' 2021-09-19 21:08:16 +09:00
semantic-release-bot
f858f025fe chore(release): 1.25.0 2021-06-10 10:25:49 +00:00
sct
c92bb134d4 Merge branch 'develop' 2021-06-10 19:18:12 +09:00
semantic-release-bot
7e0e2a92e3 chore(release): 1.24.0 2021-05-05 08:57:48 +00:00
sct
e840489e57 Merge branch 'develop' 2021-05-05 17:50:27 +09:00
sct
800f4668ec chore: fix gitattributes for images [skip ci] 2021-05-05 17:49:55 +09:00
semantic-release-bot
f2c37babeb chore(release): 1.23.2 2021-04-21 12:28:28 +00:00
sct
24095eac03 Merge branch 'develop' 2021-04-21 21:20:07 +09:00
semantic-release-bot
8c782e0c93 chore(release): 1.23.1 2021-04-16 14:19:53 +00:00
sct
08ce7f24b8 Merge branch 'develop' 2021-04-16 23:11:54 +09:00
semantic-release-bot
67e794aca4 chore(release): 1.23.0 2021-04-16 12:38:05 +00:00
sct
c9c429d027 Merge branch 'develop' 2021-04-16 21:30:30 +09:00
semantic-release-bot
31ac11f4eb chore(release): 1.22.0 2021-04-01 06:33:50 +00:00
sct
f072a4e11d Merge branch 'develop' 2021-04-01 15:27:10 +09:00
semantic-release-bot
b24c23e549 chore(release): 1.21.1 2021-03-15 08:04:26 +00:00
sct
e602e9f758 Merge branch 'develop' 2021-03-15 07:56:33 +00:00
semantic-release-bot
afd46ebee8 chore(release): 1.21.0 2021-03-15 02:00:59 +00:00
sct
be47d1e56c Merge branch 'develop' 2021-03-15 01:52:10 +00:00
semantic-release-bot
0e612f01f1 chore(release): 1.20.1 2021-02-28 04:10:48 +00:00
sct
cd9d9ae8d2 Merge branch 'develop' 2021-02-28 04:03:43 +00:00
semantic-release-bot
ea2765f8c7 chore(release): 1.20.0 2021-02-23 12:33:56 +00:00
sct
448e7c3a7d Merge branch 'develop' 2021-02-23 12:26:58 +00:00
sct
6ab3cd77a7 Merge branch 'develop' 2021-02-23 12:09:08 +00:00
semantic-release-bot
3dfbb0e0a5 chore(release): 1.19.1 2021-02-06 05:14:36 +00:00
TheCatLady
726f62b9b6 fix(ui): Fix webhook URL validation regex (#864) 2021-02-06 14:08:24 +09:00
sct
81c30347f6 Merge branch 'develop' 2021-02-05 14:15:02 +00:00
semantic-release-bot
a397893217 chore(release): 1.19.0 2021-02-05 10:32:05 +00:00
sct
12c5bbd778 Merge branch 'develop' 2021-02-05 10:25:43 +00:00
sct
06261c1118 ci(snapcraft): remove --unshallow argument from fetch in Prepare step 2021-01-30 06:31:34 +00:00
semantic-release-bot
686fe6fdf8 chore(release): 1.18.0 2021-01-30 06:13:16 +00:00
sct
c80a1fa261 Merge branch 'develop' 2021-01-30 06:06:49 +00:00
semantic-release-bot
6167011ee5 chore(release): 1.17.2 2021-01-20 07:27:33 +00:00
sct
bc399d95a0 Merge branch 'develop' 2021-01-20 07:21:48 +00:00
semantic-release-bot
519dd17d37 chore(release): 1.17.1 2021-01-19 10:24:57 +00:00
sct
0761593411 Merge branch 'develop' 2021-01-19 10:18:51 +00:00
semantic-release-bot
caa45415b1 chore(release): 1.17.0 2021-01-19 07:58:37 +00:00
sct
ed297d9d6c Merge branch 'develop' 2021-01-19 07:52:47 +00:00
semantic-release-bot
c6486c3643 chore(release): 1.16.0 2021-01-07 09:58:09 +00:00
sct
f9695068a8 Merge branch 'develop' 2021-01-07 09:53:04 +00:00
semantic-release-bot
fd90adba4d chore(release): 1.15.0 2021-01-04 12:01:25 +00:00
sct
970aa612fe Merge pull request #570 from sct/release/release-20210104
Release
2021-01-04 20:56:34 +09:00
sct
71169944dc Merge branch 'develop' into release/release-20210104 2021-01-04 11:54:45 +00:00
semantic-release-bot
d77f9eb44e chore(release): 1.14.1 2021-01-02 08:57:24 +00:00
sct
22f2037ea6 fix(holiday): remove special holiday slider 2021-01-02 08:33:24 +00:00
semantic-release-bot
2cdf755c20 chore(release): 1.14.0 2020-12-25 11:52:44 +00:00
sct
f99ab47c01 Merge branch 'develop' 2020-12-25 20:47:19 +09:00
semantic-release-bot
7e4175ff21 chore(release): 1.13.0 2020-12-23 12:44:01 +00:00
sct
4157432aa7 Merge branch 'develop' 2020-12-23 21:39:00 +09:00
semantic-release-bot
741b2c4e9f chore(release): 1.12.1 2020-12-22 03:17:58 +00:00
sct
6dbe9ea814 Merge branch 'develop' 2020-12-22 12:12:25 +09:00
semantic-release-bot
96328ab5c6 chore(release): 1.12.0 2020-12-22 02:26:31 +00:00
sct
c31709681a Merge branch 'develop' 2020-12-22 11:21:19 +09:00
semantic-release-bot
0a3d7c5915 chore(release): 1.11.0 2020-12-20 01:18:39 +00:00
sct
912c63e562 Merge branch 'develop' 2020-12-20 10:12:59 +09:00
semantic-release-bot
077513151e chore(release): 1.10.0 2020-12-19 04:54:22 +00:00
sct
191f39e8bd Merge branch 'develop' 2020-12-19 13:49:05 +09:00
semantic-release-bot
048b6c1d90 chore(release): 1.9.1 2020-12-18 16:08:58 +00:00
sct
a6a065ac0b Merge branch 'develop' 2020-12-19 01:03:48 +09:00
semantic-release-bot
abb0b2d763 chore(release): 1.9.0 2020-12-18 12:05:35 +00:00
sct
d2668d3f49 Merge branch 'develop' 2020-12-18 21:00:48 +09:00
semantic-release-bot
bee6eb288f chore(release): 1.8.0 2020-12-17 15:54:40 +00:00
sct
3473aed27e Merge branch 'develop' 2020-12-17 15:49:18 +00:00
semantic-release-bot
a9144a2167 chore(release): 1.7.0 2020-12-17 14:09:58 +00:00
sct
59822d6e42 Merge branch 'develop' 2020-12-17 14:05:24 +00:00
semantic-release-bot
0bd1bee1fa chore(release): 1.6.0 2020-12-16 05:56:57 +00:00
sct
46326f9d16 Merge branch 'develop' 2020-12-16 05:52:26 +00:00
semantic-release-bot
54b96cc451 chore(release): 1.5.0 2020-12-15 09:05:37 +00:00
sct
7d2a187865 Merge branch 'develop' 2020-12-15 09:00:44 +00:00
semantic-release-bot
17df5dd072 chore(release): 1.4.0 2020-12-15 00:53:05 +00:00
sct
4b48a36ad3 Merge branch 'develop' 2020-12-15 00:48:28 +00:00
semantic-release-bot
16bd3e9fd5 chore(release): 1.3.2 2020-12-14 13:11:15 +00:00
sct
6360c82898 Merge branch 'develop' 2020-12-14 13:06:22 +00:00
semantic-release-bot
f1df9eeb2f chore(release): 1.3.1 2020-12-14 08:03:51 +00:00
sct
a3973791fb Merge branch 'develop' 2020-12-14 07:59:18 +00:00
semantic-release-bot
157ab80be1 chore(release): 1.3.0 2020-12-14 05:32:31 +00:00
sct
ac0f95b773 Merge branch 'develop' 2020-12-14 05:27:01 +00:00
semantic-release-bot
45ebfb937e chore(release): 1.2.0 2020-12-11 22:55:12 +00:00
sct
85325cda7f Merge branch 'develop' 2020-12-11 22:50:33 +00:00
semantic-release-bot
5fc02d0d94 chore(release): 1.1.0 2020-12-08 08:21:58 +00:00
sct
16902f85fb Merge branch 'develop' 2020-12-08 08:16:24 +00:00
semantic-release-bot
17b5a0bf9c chore(release): 1.0.0 2020-12-06 01:58:28 +00:00
33 changed files with 1497 additions and 653 deletions

View File

@@ -9,6 +9,8 @@ jobs:
semantic-release:
name: Tag and release latest version
runs-on: ubuntu-20.04
env:
HUSKY: 0
steps:
- name: Checkout
uses: actions/checkout@v3

3
.gitignore vendored
View File

@@ -64,6 +64,3 @@ cypress/screenshots
# TS Build Info
tsconfig.tsbuildinfo
# Config Cache Directory
config/cache

1352
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -138,7 +138,6 @@ location ^~ /overseerr {
sub_filter 'href="/"' 'href="/$app"';
sub_filter 'href="/login"' 'href="/$app/login"';
sub_filter 'href:"/"' 'href:"/$app"';
sub_filter '\/_next' '\/$app\/_next';
sub_filter '/_next' '/$app/_next';
sub_filter '/api/v1' '/$app/api/v1';
sub_filter '/login/plex/loading' '/$app/login/plex/loading';

View File

@@ -4585,13 +4585,9 @@ paths:
type: number
example: 123
seasons:
oneOf:
- type: array
items:
type: number
minimum: 1
- type: string
enum: [all]
type: array
items:
type: number
is4k:
type: boolean
example: false
@@ -4670,7 +4666,7 @@ paths:
$ref: '#/components/schemas/MediaRequest'
put:
summary: Update MediaRequest
description: Updates a specific media request and returns the request in a JSON object. Requires the `MANAGE_REQUESTS` permission.
description: Updates a specific media request and returns the request in a JSON object.. Requires the `MANAGE_REQUESTS` permission.
tags:
- request
parameters:
@@ -4681,37 +4677,6 @@ paths:
example: '1'
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
mediaType:
type: string
enum: [movie, tv]
seasons:
type: array
items:
type: number
minimum: 1
is4k:
type: boolean
example: false
serverId:
type: number
profileId:
type: number
rootFolder:
type: string
languageProfileId:
type: number
userId:
type: number
nullable: true
required:
- mediaType
responses:
'200':
description: Succesfully updated request

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "0.1.0",
"version": "1.30.0",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",

View File

@@ -191,7 +191,7 @@ export interface TmdbVideo {
export interface TmdbTvEpisodeResult {
id: number;
air_date: string | null;
air_date: string;
episode_number: number;
name: string;
overview: string;
@@ -372,8 +372,7 @@ export interface TmdbPersonCombinedCredits {
crew: TmdbPersonCreditCrew[];
}
export interface TmdbSeasonWithEpisodes
extends Omit<TmdbTvSeasonResult, 'episode_count'> {
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
episodes: TmdbTvEpisodeResult[];
external_ids: TmdbExternalIds;
}

View File

@@ -17,7 +17,6 @@ import WebPushAgent from '@server/lib/notifications/agents/webpush';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import routes from '@server/routes';
import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
@@ -177,9 +176,6 @@ app
next();
});
server.use('/api/v1', routes);
server.use('/imageproxy', imageproxy);
server.get('*', (req, res) => handle(req, res));
server.use(
(

View File

@@ -51,11 +51,6 @@ export interface CacheItem {
};
}
export interface CacheResponse {
apiCaches: CacheItem[];
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
}
export interface StatusResponse {
version: string;
commitTag: string;

View File

@@ -1,5 +1,4 @@
import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy';
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
import { radarrScanner } from '@server/lib/scanners/radarr';
import { sonarrScanner } from '@server/lib/scanners/sonarr';
@@ -64,7 +63,7 @@ export const startJobs = (): void => {
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'short',
interval: 'long',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
@@ -134,21 +133,5 @@ export const startJobs = (): void => {
}),
});
// Run watchlist sync every 5 minutes
scheduledJobs.push({
id: 'image-cache-cleanup',
name: 'Image Cache Cleanup',
type: 'process',
interval: 'long',
cronSchedule: jobs['image-cache-cleanup'].schedule,
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
logger.info('Starting scheduled job: Image Cache Cleanup', {
label: 'Jobs',
});
// Clean TMDB image cache
ImageProxy.clearCache('tmdb');
}),
});
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
};

View File

@@ -1,268 +0,0 @@
import logger from '@server/logger';
import axios from 'axios';
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
import { createHash } from 'crypto';
import { promises } from 'fs';
import path, { join } from 'path';
type ImageResponse = {
meta: {
revalidateAfter: number;
curRevalidate: number;
isStale: boolean;
etag: string;
extension: string;
cacheKey: string;
cacheMiss: boolean;
};
imageBuffer: Buffer;
};
class ImageProxy {
public static async clearCache(key: string) {
let deletedImages = 0;
const cacheDirectory = path.join(
__dirname,
'../../config/cache/images/',
key
);
const files = await promises.readdir(cacheDirectory);
for (const file of files) {
const filePath = path.join(cacheDirectory, file);
const stat = await promises.lstat(filePath);
if (stat.isDirectory()) {
const imageFiles = await promises.readdir(filePath);
for (const imageFile of imageFiles) {
const [, expireAtSt] = imageFile.split('.');
const expireAt = Number(expireAtSt);
const now = Date.now();
if (now > expireAt) {
await promises.rm(path.join(filePath, imageFile));
deletedImages += 1;
}
}
}
}
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
label: 'Image Cache',
});
}
public static async getImageStats(
key: string
): Promise<{ size: number; imageCount: number }> {
const cacheDirectory = path.join(
__dirname,
'../../config/cache/images/',
key
);
const imageTotalSize = await ImageProxy.getDirectorySize(cacheDirectory);
const imageCount = await ImageProxy.getImageCount(cacheDirectory);
return {
size: imageTotalSize,
imageCount,
};
}
private static async getDirectorySize(dir: string): Promise<number> {
const files = await promises.readdir(dir, {
withFileTypes: true,
});
const paths = files.map(async (file) => {
const path = join(dir, file.name);
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
if (file.isFile()) {
const { size } = await promises.stat(path);
return size;
}
return 0;
});
return (await Promise.all(paths))
.flat(Infinity)
.reduce((i, size) => i + size, 0);
}
private static async getImageCount(dir: string) {
const files = await promises.readdir(dir);
return files.length;
}
private axios;
private cacheVersion;
private key;
constructor(
key: string,
baseUrl: string,
options: {
cacheVersion?: number;
rateLimitOptions?: rateLimitOptions;
} = {}
) {
this.cacheVersion = options.cacheVersion ?? 1;
this.key = key;
this.axios = axios.create({
baseURL: baseUrl,
});
if (options.rateLimitOptions) {
this.axios = rateLimit(this.axios, options.rateLimitOptions);
}
}
public async getImage(path: string): Promise<ImageResponse> {
const cacheKey = this.getCacheKey(path);
const imageResponse = await this.get(cacheKey);
if (!imageResponse) {
const newImage = await this.set(path, cacheKey);
if (!newImage) {
throw new Error('Failed to load image');
}
return newImage;
}
// If the image is stale, we will revalidate it in the background.
if (imageResponse.meta.isStale) {
this.set(path, cacheKey);
}
return imageResponse;
}
private async get(cacheKey: string): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const files = await promises.readdir(directory);
const now = Date.now();
for (const file of files) {
const [maxAgeSt, expireAtSt, etag, extension] = file.split('.');
const buffer = await promises.readFile(join(directory, file));
const expireAt = Number(expireAtSt);
const maxAge = Number(maxAgeSt);
return {
meta: {
curRevalidate: maxAge,
revalidateAfter: maxAge * 1000 + now,
isStale: now > expireAt,
etag,
extension,
cacheKey,
cacheMiss: false,
},
imageBuffer: buffer,
};
}
} catch (e) {
// No files. Treat as empty cache.
}
return null;
}
private async set(
path: string,
cacheKey: string
): Promise<ImageResponse | null> {
try {
const directory = join(this.getCacheDirectory(), cacheKey);
const response = await this.axios.get(path, {
responseType: 'arraybuffer',
});
const buffer = Buffer.from(response.data, 'binary');
const extension = path.split('.').pop() ?? '';
const maxAge = Number(response.headers['cache-control'].split('=')[1]);
const expireAt = Date.now() + maxAge * 1000;
const etag = response.headers.etag.replace(/"/g, '');
await this.writeToCacheDir(
directory,
extension,
maxAge,
expireAt,
buffer,
etag
);
return {
meta: {
curRevalidate: maxAge,
revalidateAfter: expireAt,
isStale: false,
etag,
extension,
cacheKey,
cacheMiss: true,
},
imageBuffer: buffer,
};
} catch (e) {
logger.debug('Something went wrong caching image.', {
label: 'Image Cache',
errorMessage: e.message,
});
return null;
}
}
private async writeToCacheDir(
dir: string,
extension: string,
maxAge: number,
expireAt: number,
buffer: Buffer,
etag: string
) {
const filename = join(dir, `${maxAge}.${expireAt}.${etag}.${extension}`);
await promises.rm(dir, { force: true, recursive: true }).catch(() => {
// do nothing
});
await promises.mkdir(dir, { recursive: true });
await promises.writeFile(filename, buffer);
}
private getCacheKey(path: string) {
return this.getHash([this.key, this.cacheVersion, path]);
}
private getHash(items: (string | number | Buffer)[]) {
const hash = createHash('sha256');
for (const item of items) {
if (typeof item === 'number') hash.update(String(item));
else {
hash.update(item);
}
}
// See https://en.wikipedia.org/wiki/Base64#Filenames
return hash.digest('base64').replace(/\//g, '-');
}
private getCacheDirectory() {
return path.join(__dirname, '../../config/cache/images/', this.key);
}
}
export default ImageProxy;

View File

@@ -247,8 +247,7 @@ export type JobId =
| 'radarr-scan'
| 'sonarr-scan'
| 'download-sync'
| 'download-sync-reset'
| 'image-cache-cleanup';
| 'download-sync-reset';
interface AllSettings {
clientId: string;
@@ -415,9 +414,6 @@ class Settings {
'download-sync-reset': {
schedule: '0 0 1 * * *',
},
'image-cache-cleanup': {
schedule: '0 0 5 * * *',
},
},
};
if (initialSettings) {

View File

@@ -29,7 +29,7 @@ import type { Video } from './Movie';
interface Episode {
id: number;
name: string;
airDate: string | null;
airDate: string;
episodeNumber: number;
overview: string;
productionCode: string;
@@ -50,7 +50,7 @@ interface Season {
seasonNumber: number;
}
export interface SeasonWithEpisodes extends Omit<Season, 'episodeCount'> {
export interface SeasonWithEpisodes extends Season {
episodes: Episode[];
externalIds: ExternalIds;
}
@@ -141,6 +141,7 @@ export const mapSeasonWithEpisodes = (
season: TmdbSeasonWithEpisodes
): SeasonWithEpisodes => ({
airDate: season.air_date,
episodeCount: season.episode_count,
episodes: season.episodes.map(mapEpisodeResult),
externalIds: mapExternalIds(season.external_ids),
id: season.id,

View File

@@ -1,39 +0,0 @@
import ImageProxy from '@server/lib/imageproxy';
import logger from '@server/logger';
import { Router } from 'express';
const router = Router();
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
rateLimitOptions: {
maxRequests: 20,
maxRPS: 50,
},
});
/**
* Image Proxy
*/
router.get('/*', async (req, res) => {
const imagePath = req.path.replace('/image', '');
try {
const imageData = await tmdbImageProxy.getImage(imagePath);
res.writeHead(200, {
'Content-Type': `image/${imageData.meta.extension}`,
'Content-Length': imageData.imageBuffer.length,
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
'OS-Cache-Key': imageData.meta.cacheKey,
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
});
res.end(imageData.imageBuffer);
} catch (e) {
logger.error('Failed to proxy image', {
imagePath,
errorMessage: e.message,
});
res.status(500).send();
}
});
export default router;

View File

@@ -1,7 +1,5 @@
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus } from '@server/constants/media';
import Media from '@server/entity/Media';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import {
mapCastCredits,
@@ -36,7 +34,6 @@ personRoutes.get('/:id', async (req, res, next) => {
personRoutes.get('/:id/combined_credits', async (req, res, next) => {
const tmdb = new TheMovieDb();
const settings = getSettings();
try {
const combinedCredits = await tmdb.getPersonCombinedCredits({
@@ -44,30 +41,14 @@ personRoutes.get('/:id/combined_credits', async (req, res, next) => {
language: req.locale ?? (req.query.language as string),
});
let castMedia = await Media.getRelatedMedia(
const castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id)
);
let crewMedia = await Media.getRelatedMedia(
const crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id)
);
if (settings.main.hideAvailable) {
castMedia = castMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
crewMedia = crewMedia.filter(
(media) =>
(media.mediaType === 'movie' || media.mediaType === 'tv') &&
media.status !== MediaStatus.AVAILABLE &&
media.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
return res.status(200).json({
cast: combinedCredits.cast
.map((result) =>

View File

@@ -14,10 +14,9 @@ import type {
import { scheduledJobs } from '@server/job/schedule';
import type { AvailableCacheIds } from '@server/lib/cache';
import cacheManager from '@server/lib/cache';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions';
import { plexFullScanner } from '@server/lib/scanners/plex';
import type { JobId, MainSettings } from '@server/lib/settings';
import type { MainSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
@@ -492,7 +491,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
});
});
settingsRoutes.post<{ jobId: JobId }>(
settingsRoutes.post<{ jobId: string }>(
'/jobs/:jobId/cancel',
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
@@ -519,7 +518,7 @@ settingsRoutes.post<{ jobId: JobId }>(
}
);
settingsRoutes.post<{ jobId: JobId }>(
settingsRoutes.post<{ jobId: string }>(
'/jobs/:jobId/schedule',
(req, res, next) => {
const scheduledJob = scheduledJobs.find(
@@ -554,23 +553,16 @@ settingsRoutes.post<{ jobId: JobId }>(
}
);
settingsRoutes.get('/cache', async (_req, res) => {
const cacheManagerCaches = cacheManager.getAllCaches();
settingsRoutes.get('/cache', (req, res) => {
const caches = cacheManager.getAllCaches();
const apiCaches = Object.values(cacheManagerCaches).map((cache) => ({
id: cache.id,
name: cache.name,
stats: cache.getStats(),
}));
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
return res.status(200).json({
apiCaches,
imageCache: {
tmdb: tmdbImageCache,
},
});
return res.status(200).json(
Object.values(caches).map((cache) => ({
id: cache.id,
name: cache.name,
stats: cache.getStats(),
}))
);
});
settingsRoutes.post<{ cacheId: AvailableCacheIds }>(

View File

@@ -61,7 +61,7 @@ function Button<P extends ElementTypes = 'button'>(
break;
case 'warning':
buttonStyle.push(
'text-white border border-yellow-500 bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700'
'text-white border border-yellow-500 backdrop-blur bg-yellow-500 bg-opacity-80 hover:bg-opacity-100 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-opacity-100 active:border-yellow-700'
);
break;
case 'success':

View File

@@ -1,27 +1,18 @@
import useSettings from '@app/hooks/useSettings';
import type { ImageLoader, ImageProps } from 'next/image';
import type { ImageProps } from 'next/image';
import Image from 'next/image';
const imageLoader: ImageLoader = ({ src }) => src;
/**
* The CachedImage component should be used wherever
* we want to offer the option to locally cache images.
*
* It uses the `next/image` Image component but overrides
* the `unoptimized` prop based on the application setting `cacheImages`.
**/
const CachedImage = ({ src, ...props }: ImageProps) => {
const CachedImage = (props: ImageProps) => {
const { currentSettings } = useSettings();
let imageUrl = src;
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
const parsedUrl = new URL(imageUrl);
if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) {
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
}
}
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
};
export default CachedImage;

View File

@@ -20,7 +20,7 @@ const Tooltip = ({ children, content, tooltipConfig }: TooltipProps) => {
return (
<>
{React.cloneElement(children, { ref: setTriggerRef })}
{visible && content && (
{visible && (
<div
ref={setTooltipRef}
{...getTooltipProps({

View File

@@ -1,4 +1,3 @@
import CachedImage from '@app/components/Common/CachedImage';
import Link from 'next/link';
import { useState } from 'react';
@@ -31,15 +30,11 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
role="link"
tabIndex={0}
>
<div className="relative h-full w-full">
<CachedImage
src={image}
alt={name}
className="relative z-40 h-full w-full"
layout="fill"
objectFit="contain"
/>
</div>
<img
src={image}
alt={name}
className="relative z-40 max-h-full max-w-full"
/>
<div
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'

View File

@@ -7,7 +7,6 @@ import PageTitle from '@app/components/Common/PageTitle';
import IssueComment from '@app/components/IssueDetails/IssueComment';
import IssueDescription from '@app/components/IssueDetails/IssueDescription';
import { issueOptions } from '@app/components/IssueModal/constants';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
@@ -89,13 +88,6 @@ const IssueDetails = () => {
: null
);
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
const CommentSchema = Yup.object().shape({
message: Yup.string().required(),
});
@@ -362,7 +354,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl && (
<Button
as="a"
href={plexUrl}
href={issueData?.media.plexUrl}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -395,7 +387,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl4k && (
<Button
as="a"
href={plexUrl4k}
href={issueData?.media.plexUrl4k}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -598,7 +590,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl && (
<Button
as="a"
href={plexUrl}
href={issueData?.media.plexUrl}
target="_blank"
rel="noreferrer"
className="w-full"
@@ -631,7 +623,7 @@ const IssueDetails = () => {
{issueData?.media.plexUrl4k && (
<Button
as="a"
href={plexUrl4k}
href={issueData?.media.plexUrl4k}
target="_blank"
rel="noreferrer"
className="w-full"

View File

@@ -120,7 +120,7 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
>
<>
<div className="sidebar relative flex h-full w-full max-w-xs flex-1 flex-col bg-gray-800">
<div className="sidebar-close-button absolute right-0 -mr-14 p-1">
<div className="sidebar-close-button absolute top-0 right-0 -mr-14 p-1">
<button
className="flex h-12 w-12 items-center justify-center rounded-full focus:bg-gray-600 focus:outline-none"
aria-label="Close sidebar"

View File

@@ -1,8 +1,6 @@
import TitleCard from '@app/components/TitleCard';
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
@@ -17,18 +15,6 @@ interface ShowMoreCardProps {
const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
const intl = useIntl();
const [isHovered, setHovered] = useState(false);
const { ref, inView } = useInView({
triggerOnce: true,
});
if (!inView) {
return (
<div ref={ref}>
<TitleCard.Placeholder />
</div>
);
}
return (
<Link href={url}>
<a

View File

@@ -18,7 +18,6 @@ import PersonCard from '@app/components/PersonCard';
import RequestButton from '@app/components/RequestButton';
import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -124,12 +123,29 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]);
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.plexUrl);
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.plexUrl4k);
useEffect(() => {
if (data) {
if (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.plexUrl);
setPlexUrl4k(data.mediaInfo?.plexUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.plexUrl,
data?.mediaInfo?.plexUrl4k,
]);
if (!data && !error) {
return <LoadingSpinner />;
@@ -308,7 +324,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie"
plexUrl={plexUrl}
plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl}
/>
{settings.currentSettings.movie4kEnabled &&
@@ -330,7 +346,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
}
tmdbId={data.mediaInfo?.tmdbId}
mediaType="movie"
plexUrl={plexUrl4k}
plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
/>
)}

View File

@@ -4,7 +4,6 @@ import CachedImage from '@app/components/Common/CachedImage';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { withProperties } from '@app/utils/typeHelpers';
@@ -62,13 +61,6 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
const { hasPermission } = useUser();
const intl = useIntl();
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
@@ -146,7 +138,11 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
).length > 0
}
is4k={requestData.is4k}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
plexUrl={
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
@@ -221,13 +217,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
fallbackData: request,
});
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
@@ -407,7 +396,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
plexUrl={
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k

View File

@@ -4,7 +4,6 @@ import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import {
@@ -62,13 +61,6 @@ const RequestItemError = ({
revalidateList();
};
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
return (
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
@@ -138,7 +130,11 @@ const RequestItemError = ({
).length > 0
}
is4k={requestData.is4k}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
plexUrl={
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k
@@ -320,13 +316,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
}
};
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: requestData?.media?.plexUrl,
plexUrl4k: requestData?.media?.plexUrl4k,
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
if (!title && !error) {
return (
<div
@@ -473,7 +462,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
plexUrl={
requestData.is4k
? requestData.media.plexUrl4k
: requestData.media.plexUrl
}
serviceUrl={
requestData.is4k
? requestData.media.serviceUrl4k

View File

@@ -11,10 +11,7 @@ import { formatBytes } from '@app/utils/numberHelpers';
import { Transition } from '@headlessui/react';
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
import { PencilIcon } from '@heroicons/react/solid';
import type {
CacheItem,
CacheResponse,
} from '@server/interfaces/api/settingsInterfaces';
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
import type { JobId } from '@server/lib/settings';
import axios from 'axios';
import cronstrue from 'cronstrue/i18n';
@@ -57,7 +54,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'sonarr-scan': 'Sonarr Scan',
'download-sync': 'Download Sync',
'download-sync-reset': 'Download Sync Reset',
'image-cache-cleanup': 'Image Cache Cleanup',
editJobSchedule: 'Modify Job',
jobScheduleEditSaved: 'Job edited successfully!',
jobScheduleEditFailed: 'Something went wrong while saving the job.',
@@ -67,11 +63,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
editJobScheduleSelectorMinutes:
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
imagecache: 'Image Cache',
imagecacheDescription:
'When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.',
imagecachecount: 'Images Cached',
imagecachesize: 'Total Cache Size',
});
interface Job {
@@ -137,8 +128,7 @@ const SettingsJobs = () => {
} = useSWR<Job[]>('/api/v1/settings/jobs', {
refreshInterval: 5000,
});
const { data: appData } = useSWR('/api/v1/status/appdata');
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheResponse>(
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheItem[]>(
'/api/v1/settings/cache',
{
refreshInterval: 10000,
@@ -440,7 +430,7 @@ const SettingsJobs = () => {
</tr>
</thead>
<Table.TBody>
{cacheData?.apiCaches.map((cache) => (
{cacheData?.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
@@ -459,41 +449,6 @@ const SettingsJobs = () => {
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description">
{intl.formatMessage(messages.imagecacheDescription, {
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
appDataPath: appData ? appData.appDataPath : '/app/config',
})}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.imagecachecount)}
</Table.TH>
<Table.TH>{intl.formatMessage(messages.imagecachesize)}</Table.TH>
</tr>
</thead>
<Table.TBody>
<tr>
<Table.TD>The Movie Database (tmdb)</Table.TD>
<Table.TD>
{intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)}
</Table.TD>
<Table.TD>
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
</Table.TD>
</tr>
</Table.TBody>
</Table>
</div>
</>
);
};

View File

@@ -309,7 +309,7 @@ const SettingsMain = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="cacheImages" className="checkbox-label">
<label htmlFor="csrfProtection" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.cacheImages)}
</span>

View File

@@ -77,7 +77,7 @@ const StatusBadge = ({
mediaType === 'movie' ? globalMessages.movie : globalMessages.tvshow
),
});
} else if (hasPermission(Permission.ADMIN) && serviceUrl) {
} else if (hasPermission(Permission.ADMIN)) {
mediaLink = serviceUrl;
mediaLinkDescription = intl.formatMessage(messages.openinarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',

View File

@@ -6,7 +6,6 @@ import useSWR from 'swr';
const messages = defineMessages({
somethingwentwrong: 'Something went wrong while retrieving season data.',
noepisodes: 'Episode list unavailable.',
});
type SeasonProps = {
@@ -30,38 +29,32 @@ const Season = ({ seasonNumber, tvId }: SeasonProps) => {
return (
<div className="flex flex-col justify-center divide-y divide-gray-700">
{data.episodes.length === 0 ? (
<p>{intl.formatMessage(messages.noepisodes)}</p>
) : (
data.episodes
.slice()
.reverse()
.map((episode) => {
return (
<div
className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
key={`season-${seasonNumber}-episode-${episode.episodeNumber}`}
>
<div className="flex-1">
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2">
<h3 className="text-lg">{episode.name}</h3>
{episode.airDate && (
<AirDateBadge airDate={episode.airDate} />
)}
</div>
{episode.overview && <p>{episode.overview}</p>}
{data.episodes
.slice()
.reverse()
.map((episode) => {
return (
<div
className="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4"
key={`season-${seasonNumber}-episode-${episode.episodeNumber}`}
>
<div className="flex-1">
<div className="flex flex-col space-y-2 xl:flex-row xl:items-center xl:space-y-0 xl:space-x-2">
<h3 className="text-lg">{episode.name}</h3>
<AirDateBadge airDate={episode.airDate} />
</div>
{episode.stillPath && (
<img
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto"
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
{episode.overview && <p>{episode.overview}</p>}
</div>
);
})
)}
{episode.stillPath && (
<img
className="h-auto w-full rounded-lg xl:h-32 xl:w-auto"
src={`https://image.tmdb.org/t/p/original/${episode.stillPath}`}
alt=""
/>
)}
</div>
);
})}
</div>
);
};

View File

@@ -22,7 +22,6 @@ import RequestModal from '@app/components/RequestModal';
import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge';
import Season from '@app/components/TvDetails/Season';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -123,12 +122,29 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]);
const { plexUrl, plexUrl4k } = useDeepLinks({
plexUrl: data?.mediaInfo?.plexUrl,
plexUrl4k: data?.mediaInfo?.plexUrl4k,
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
});
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.plexUrl);
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.plexUrl4k);
useEffect(() => {
if (data) {
if (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
} else {
setPlexUrl(data.mediaInfo?.plexUrl);
setPlexUrl4k(data.mediaInfo?.plexUrl4k);
}
}
}, [
data,
data?.mediaInfo?.iOSPlexUrl,
data?.mediaInfo?.iOSPlexUrl4k,
data?.mediaInfo?.plexUrl,
data?.mediaInfo?.plexUrl4k,
]);
if (!data && !error) {
return <LoadingSpinner />;
@@ -321,7 +337,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv"
plexUrl={plexUrl}
plexUrl={data.mediaInfo?.plexUrl}
serviceUrl={data.mediaInfo?.serviceUrl}
/>
{settings.currentSettings.series4kEnabled &&
@@ -343,7 +359,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
}
tmdbId={data.mediaInfo?.tmdbId}
mediaType="tv"
plexUrl={plexUrl4k}
plexUrl={data.mediaInfo?.plexUrl4k}
serviceUrl={data.mediaInfo?.serviceUrl4k}
/>
)}
@@ -813,7 +829,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</div>
)}
{data.nextEpisodeToAir &&
data.nextEpisodeToAir.airDate &&
data.nextEpisodeToAir.airDate !== data.firstAirDate && (
<div className="media-fact">
<span>{intl.formatMessage(messages.nextAirDate)}</span>

View File

@@ -1,35 +0,0 @@
import { useEffect, useState } from 'react';
interface useDeepLinksProps {
plexUrl?: string;
plexUrl4k?: string;
iOSPlexUrl?: string;
iOSPlexUrl4k?: string;
}
const useDeepLinks = ({
plexUrl,
plexUrl4k,
iOSPlexUrl,
iOSPlexUrl4k,
}: useDeepLinksProps) => {
const [returnedPlexUrl, setReturnedPlexUrl] = useState(plexUrl);
const [returnedPlexUrl4k, setReturnedPlexUrl4k] = useState(plexUrl4k);
useEffect(() => {
if (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)
) {
setReturnedPlexUrl(iOSPlexUrl);
setReturnedPlexUrl4k(iOSPlexUrl4k);
} else {
setReturnedPlexUrl(plexUrl);
setReturnedPlexUrl4k(plexUrl4k);
}
}, [iOSPlexUrl, iOSPlexUrl4k, plexUrl, plexUrl4k]);
return { plexUrl: returnedPlexUrl, plexUrl4k: returnedPlexUrl4k };
};
export default useDeepLinks;

View File

@@ -881,7 +881,6 @@
"components.TitleCard.mediaerror": "{mediaType} Not Found",
"components.TitleCard.tmdbid": "TMDB ID",
"components.TitleCard.tvdbid": "TheTVDB ID",
"components.TvDetails.Season.noepisodes": "Episode list unavailable.",
"components.TvDetails.Season.somethingwentwrong": "Something went wrong while retrieving season data.",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
"components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew",