Compare commits
19 Commits
v2.5.0
...
preview-go
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54e3df2494 | ||
|
|
f5b3a526cb | ||
|
|
e5ab847547 | ||
|
|
40539cc4b1 | ||
|
|
0bd6d57834 | ||
|
|
f884ac9c66 | ||
|
|
c2d9d00b41 | ||
|
|
77a36f9714 | ||
|
|
f773e0fb2a | ||
|
|
767a24164d | ||
|
|
8394eb5ad4 | ||
|
|
b8425d6388 | ||
|
|
ebb7f00305 | ||
|
|
418d51590d | ||
|
|
a6dd4a8fed | ||
|
|
4d1163c343 | ||
|
|
b085e12ff9 | ||
|
|
33e7a153aa | ||
|
|
9891a7577c |
@@ -58,12 +58,27 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
|||||||
|
|
||||||
- Be sure to follow both the [code](#contributing-code) and [UI text](#ui-text-style) guidelines.
|
- Be sure to follow both the [code](#contributing-code) and [UI text](#ui-text-style) guidelines.
|
||||||
- Should you need to update your fork, you can do so by rebasing from `upstream`:
|
- Should you need to update your fork, you can do so by rebasing from `upstream`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch upstream
|
git fetch upstream
|
||||||
git rebase upstream/develop
|
git rebase upstream/develop
|
||||||
git push origin BRANCH_NAME -f
|
git push origin BRANCH_NAME -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Helm Chart
|
||||||
|
|
||||||
|
Tools Required:
|
||||||
|
|
||||||
|
- [Helm](https://helm.sh/docs/intro/install/)
|
||||||
|
- [helm-docs](https://github.com/norwoodj/helm-docs)
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Make the necessary changes.
|
||||||
|
2. Test your changes.
|
||||||
|
3. Update the `version` in `charts/jellyseerr-chart/Chart.yaml` following [Semantic Versioning (SemVer)](https://semver.org/).
|
||||||
|
4. Run the `helm-docs` command to regenerate the chart's README.
|
||||||
|
|
||||||
### Contributing Code
|
### Contributing Code
|
||||||
|
|
||||||
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/fallenbagel/jellyseerr/issues) to avoid multiple people working on the same thing.
|
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/fallenbagel/jellyseerr/issues) to avoid multiple people working on the same thing.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ kubeVersion: ">=1.23.0-0"
|
|||||||
name: jellyseerr-chart
|
name: jellyseerr-chart
|
||||||
description: Jellyseerr helm chart for Kubernetes
|
description: Jellyseerr helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 2.2.0
|
version: 2.3.2
|
||||||
appVersion: "2.4.0"
|
appVersion: "2.5.1"
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: Jellyseerr
|
- name: Jellyseerr
|
||||||
url: https://github.com/Fallenbagel/jellyseerr
|
url: https://github.com/Fallenbagel/jellyseerr
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# jellyseerr-chart
|
# jellyseerr-chart
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
Jellyseerr helm chart for Kubernetes
|
Jellyseerr helm chart for Kubernetes
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ metadata:
|
|||||||
name: {{ include "jellyseerr.configPersistenceName" . }}
|
name: {{ include "jellyseerr.configPersistenceName" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.config.persistence.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
{{- with .Values.config.persistence.accessModes }}
|
{{- with .Values.config.persistence.accessModes }}
|
||||||
accessModes:
|
accessModes:
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ or for Cloudflare's DNS:
|
|||||||
```bash
|
```bash
|
||||||
--dns=1.1.1.1
|
--dns=1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9 DNS:
|
||||||
|
```bash
|
||||||
|
--dns=9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -45,6 +51,16 @@ services:
|
|||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
dns:
|
||||||
|
- 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -56,7 +72,7 @@ services:
|
|||||||
4. Click on Change adapter settings.
|
4. Click on Change adapter settings.
|
||||||
5. Right-click the network interface connected to the internet and select Properties.
|
5. Right-click the network interface connected to the internet and select Properties.
|
||||||
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
||||||
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS.
|
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS or `9.9.9.9` for Quad9's DNS.
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -73,6 +89,10 @@ services:
|
|||||||
```bash
|
```bash
|
||||||
nameserver 1.1.1.1
|
nameserver 1.1.1.1
|
||||||
```
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```bash
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -81,7 +101,7 @@ services:
|
|||||||
|
|
||||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||||
|
|
||||||
You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`:
|
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting. You can also add the environment variable, `FORCE_IPV4_FIRST=true`:
|
||||||
|
|
||||||
<Tabs groupId="methods" queryString>
|
<Tabs groupId="methods" queryString>
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
|
|||||||
@@ -999,7 +999,7 @@ export class MediaRequest {
|
|||||||
radarrMovie.id,
|
radarrMovie.id,
|
||||||
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||||
radarrMovie.titleSlug,
|
radarrMovie.titleSlug,
|
||||||
[this.is4k ? 'serviceId4k' : 'serviceId']: radarrMovie?.id,
|
[this.is4k ? 'serviceId4k' : 'serviceId']: radarrSettings?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
await mediaRepository.update({ id: this.media.id }, updateFields);
|
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||||
|
|||||||
@@ -404,6 +404,34 @@ class AvailabilitySync {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!showExists &&
|
||||||
|
(media.status === MediaStatus.AVAILABLE ||
|
||||||
|
media.status === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status === MediaStatus.AVAILABLE
|
||||||
|
) ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status === MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
await this.mediaUpdater(media, false, mediaServerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!showExists4k &&
|
||||||
|
(media.status4k === MediaStatus.AVAILABLE ||
|
||||||
|
media.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status4k === MediaStatus.AVAILABLE
|
||||||
|
) ||
|
||||||
|
media.seasons.some(
|
||||||
|
(season) => season.status4k === MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
await this.mediaUpdater(media, true, mediaServerType);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Figure out how to run seasonUpdater for each season
|
// TODO: Figure out how to run seasonUpdater for each season
|
||||||
|
|
||||||
if ([...finalSeasons.values()].includes(false)) {
|
if ([...finalSeasons.values()].includes(false)) {
|
||||||
@@ -423,22 +451,6 @@ class AvailabilitySync {
|
|||||||
mediaServerType
|
mediaServerType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!showExists &&
|
|
||||||
(media.status === MediaStatus.AVAILABLE ||
|
|
||||||
media.status === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
await this.mediaUpdater(media, false, mediaServerType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!showExists4k &&
|
|
||||||
(media.status4k === MediaStatus.AVAILABLE ||
|
|
||||||
media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
await this.mediaUpdater(media, true, mediaServerType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -466,6 +478,10 @@ class AvailabilitySync {
|
|||||||
{ status: MediaStatus.PARTIALLY_AVAILABLE },
|
{ status: MediaStatus.PARTIALLY_AVAILABLE },
|
||||||
{ status4k: MediaStatus.AVAILABLE },
|
{ status4k: MediaStatus.AVAILABLE },
|
||||||
{ status4k: MediaStatus.PARTIALLY_AVAILABLE },
|
{ status4k: MediaStatus.PARTIALLY_AVAILABLE },
|
||||||
|
{ seasons: { status: MediaStatus.AVAILABLE } },
|
||||||
|
{ seasons: { status: MediaStatus.PARTIALLY_AVAILABLE } },
|
||||||
|
{ seasons: { status4k: MediaStatus.AVAILABLE } },
|
||||||
|
{ seasons: { status4k: MediaStatus.PARTIALLY_AVAILABLE } },
|
||||||
];
|
];
|
||||||
|
|
||||||
let mediaPage: Media[];
|
let mediaPage: Media[];
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ class GotifyAgent
|
|||||||
const title = payload.event
|
const title = payload.event
|
||||||
? `${payload.event} - ${payload.subject}`
|
? `${payload.event} - ${payload.subject}`
|
||||||
: payload.subject;
|
: payload.subject;
|
||||||
let message = payload.message ?? '';
|
|
||||||
|
let message = payload.message ? `${payload.message} \n\n` : '';
|
||||||
|
|
||||||
if (payload.request) {
|
if (payload.request) {
|
||||||
message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`;
|
message += `\n**Requested By:** ${payload.request.requestedBy.displayName} `;
|
||||||
|
|
||||||
let status = '';
|
let status = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -73,16 +74,18 @@ class GotifyAgent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
message += `\nRequest Status: ${status}`;
|
message += `\n**Request Status:** ${status} `;
|
||||||
}
|
}
|
||||||
} else if (payload.comment) {
|
} else if (payload.comment) {
|
||||||
message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`;
|
message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message} `;
|
||||||
} else if (payload.issue) {
|
} else if (payload.issue) {
|
||||||
message += `\n\nReported By: ${payload.issue.createdBy.displayName}`;
|
message += `\n\n**Reported By:** ${payload.issue.createdBy.displayName} `;
|
||||||
message += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`;
|
message += `\n**Issue Type:** ${
|
||||||
message += `\nIssue Status: ${
|
IssueTypeName[payload.issue.issueType]
|
||||||
|
} `;
|
||||||
|
message += `\n**Issue Status:** ${
|
||||||
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
|
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
|
||||||
}`;
|
} `;
|
||||||
|
|
||||||
if (type == Notification.ISSUE_CREATED) {
|
if (type == Notification.ISSUE_CREATED) {
|
||||||
priority = 1;
|
priority = 1;
|
||||||
@@ -90,12 +93,14 @@ class GotifyAgent
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const extra of payload.extra ?? []) {
|
for (const extra of payload.extra ?? []) {
|
||||||
message += `\n\n**${extra.name}**\n${extra.value}`;
|
message += `\n\n**${extra.name}**\n${extra.value} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (applicationUrl && payload.media) {
|
if (applicationUrl && payload.media) {
|
||||||
const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
|
const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
|
||||||
message += `\n\nOpen in ${applicationTitle}(${actionUrl})`;
|
const displayUrl =
|
||||||
|
actionUrl.length > 40 ? `${actionUrl.slice(0, 41)}...` : actionUrl;
|
||||||
|
message += `\n\n**Open in ${applicationTitle}:** [${displayUrl}](${actionUrl}) `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -237,6 +237,19 @@ mediaRoutes.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
|
// check if the movie exists
|
||||||
|
try {
|
||||||
|
await (service as RadarrAPI).getMovie({
|
||||||
|
id: parseInt(
|
||||||
|
is4k
|
||||||
|
? (media.externalServiceSlug4k as string)
|
||||||
|
: (media.externalServiceSlug as string)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
// remove the movie
|
||||||
await (service as RadarrAPI).removeMovie(
|
await (service as RadarrAPI).removeMovie(
|
||||||
parseInt(
|
parseInt(
|
||||||
is4k
|
is4k
|
||||||
@@ -251,6 +264,13 @@ mediaRoutes.delete(
|
|||||||
if (!tvdbId) {
|
if (!tvdbId) {
|
||||||
throw new Error('TVDB ID not found');
|
throw new Error('TVDB ID not found');
|
||||||
}
|
}
|
||||||
|
// check if the series exists
|
||||||
|
try {
|
||||||
|
await (service as SonarrAPI).getSeriesByTvdbId(tvdbId);
|
||||||
|
} catch {
|
||||||
|
return res.status(204).send();
|
||||||
|
}
|
||||||
|
// remove the series
|
||||||
await (service as SonarrAPI).removeSerie(tvdbId);
|
await (service as SonarrAPI).removeSerie(tvdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ export default async function createCustomProxyAgent(
|
|||||||
) {
|
) {
|
||||||
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
||||||
|
|
||||||
const skipUrl = (url: string) => {
|
const skipUrl = (url: string | URL) => {
|
||||||
const hostname = new URL(url).hostname;
|
const hostname =
|
||||||
|
typeof url === 'string' ? new URL(url).hostname : url.hostname;
|
||||||
|
|
||||||
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
if (proxySettings.bypassLocalAddresses && isLocalAddress(hostname)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -38,8 +39,7 @@ export default async function createCustomProxyAgent(
|
|||||||
dispatch: Dispatcher['dispatch']
|
dispatch: Dispatcher['dispatch']
|
||||||
): Dispatcher['dispatch'] => {
|
): Dispatcher['dispatch'] => {
|
||||||
return (opts, handler) => {
|
return (opts, handler) => {
|
||||||
const url = opts.origin?.toString();
|
return opts.origin && skipUrl(opts.origin)
|
||||||
return url && skipUrl(url)
|
|
||||||
? defaultAgent.dispatch(opts, handler)
|
? defaultAgent.dispatch(opts, handler)
|
||||||
: dispatch(opts, handler);
|
: dispatch(opts, handler);
|
||||||
};
|
};
|
||||||
@@ -60,13 +60,10 @@ export default async function createCustomProxyAgent(
|
|||||||
':' +
|
':' +
|
||||||
proxySettings.port,
|
proxySettings.port,
|
||||||
token,
|
token,
|
||||||
interceptors: {
|
|
||||||
Client: [noProxyInterceptor],
|
|
||||||
},
|
|
||||||
keepAliveTimeout: 5000,
|
keepAliveTimeout: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setGlobalDispatcher(proxyAgent);
|
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to connect to the proxy: ' + e.message, {
|
logger.error('Failed to connect to the proxy: ' + e.message, {
|
||||||
label: 'Proxy',
|
label: 'Proxy',
|
||||||
@@ -95,7 +92,11 @@ export default async function createCustomProxyAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLocalAddress(hostname: string) {
|
function isLocalAddress(hostname: string) {
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
if (
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
hostname === '::1'
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,13 @@ type AirDateBadgeProps = {
|
|||||||
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
||||||
const WEEK = 1000 * 60 * 60 * 24 * 8;
|
const WEEK = 1000 * 60 * 60 * 24 * 8;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
const dAirDate = new Date(airDate);
|
const dAirDate = new Date(airDate);
|
||||||
const nowDate = new Date();
|
const nowDate = new Date();
|
||||||
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
|
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
|
||||||
|
|
||||||
const compareWeek = new Date(
|
const compareWeek = new Date(
|
||||||
alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
|
alreadyAired ? Date.now() - WEEK : Date.now() + WEEK
|
||||||
);
|
);
|
||||||
|
|
||||||
let showRelative = false;
|
let showRelative = false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
|
(alreadyAired && dAirDate.getTime() > compareWeek.getTime()) ||
|
||||||
(!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
|
(!alreadyAired && dAirDate.getTime() < compareWeek.getTime())
|
||||||
@@ -32,6 +28,10 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
showRelative = true;
|
showRelative = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const diffInDays = Math.round(
|
||||||
|
(dAirDate.getTime() - nowDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge badgeType="light">
|
<Badge badgeType="light">
|
||||||
@@ -39,7 +39,7 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
timeZone,
|
timeZone: 'UTC',
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
{showRelative && (
|
{showRelative && (
|
||||||
@@ -49,9 +49,9 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
|
|||||||
{
|
{
|
||||||
relativeTime: (
|
relativeTime: (
|
||||||
<FormattedRelativeTime
|
<FormattedRelativeTime
|
||||||
value={(dAirDate.getTime() - Date.now()) / 1000}
|
value={diffInDays}
|
||||||
|
unit="day"
|
||||||
numeric="auto"
|
numeric="auto"
|
||||||
updateIntervalInSeconds={1}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
data-form-type="password"
|
data-form-type="password"
|
||||||
data-1pignore="false"
|
data-1pignore="false"
|
||||||
data-lpignore="false"
|
data-lpignore="false"
|
||||||
data-bwignore="false"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
|||||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||||
data-1pignore="false"
|
data-1pignore="false"
|
||||||
data-lpignore="false"
|
data-lpignore="false"
|
||||||
data-bwignore="false"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -629,7 +629,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<PlayButton links={mediaLinks} />
|
<div className="z-20">
|
||||||
|
<PlayButton links={mediaLinks} />
|
||||||
|
</div>
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="movie"
|
mediaType="movie"
|
||||||
media={data.mediaInfo}
|
media={data.mediaInfo}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||||
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -293,9 +294,16 @@ const RequestItemError = ({
|
|||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
|
radarrData?: RadarrSettings[];
|
||||||
|
sonarrData?: SonarrSettings[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
const RequestItem = ({
|
||||||
|
request,
|
||||||
|
revalidateList,
|
||||||
|
radarrData,
|
||||||
|
sonarrData,
|
||||||
|
}: RequestItemProps) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
@@ -390,6 +398,23 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serviceExists = () => {
|
||||||
|
if (title?.mediaInfo) {
|
||||||
|
if (title?.mediaInfo.mediaType === MediaType.MOVIE) {
|
||||||
|
return (
|
||||||
|
radarrData?.find((radarr) => radarr.id === request.serverId) !==
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
sonarrData?.find((sonarr) => sonarr.id === request.serverId) !==
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if (!title && !error) {
|
if (!title && !error) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -697,28 +722,30 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
)}
|
)}
|
||||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<>
|
<ConfirmButton
|
||||||
<ConfirmButton
|
onClick={() => deleteRequest()}
|
||||||
onClick={() => deleteRequest()}
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
className="w-full"
|
||||||
className="w-full"
|
>
|
||||||
>
|
<TrashIcon />
|
||||||
<TrashIcon />
|
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
</ConfirmButton>
|
||||||
</ConfirmButton>
|
)}
|
||||||
<ConfirmButton
|
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||||
onClick={() => deleteMediaFile()}
|
title?.mediaInfo?.serviceId &&
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
serviceExists() && (
|
||||||
className="w-full"
|
<ConfirmButton
|
||||||
>
|
onClick={() => deleteMediaFile()}
|
||||||
<TrashIcon />
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
<span>
|
className="w-full"
|
||||||
{intl.formatMessage(messages.removearr, {
|
>
|
||||||
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
|
<TrashIcon />
|
||||||
})}
|
<span>
|
||||||
</span>
|
{intl.formatMessage(messages.removearr, {
|
||||||
</ConfirmButton>
|
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
|
||||||
</>
|
})}
|
||||||
|
</span>
|
||||||
|
</ConfirmButton>
|
||||||
)}
|
)}
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
{requestData.status === MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
FunnelIcon,
|
FunnelIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||||
|
import { Permission } from '@server/lib/permissions';
|
||||||
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -51,7 +53,7 @@ const RequestList = () => {
|
|||||||
const { user } = useUser({
|
const { user } = useUser({
|
||||||
id: Number(router.query.userId),
|
id: Number(router.query.userId),
|
||||||
});
|
});
|
||||||
const { user: currentUser } = useUser();
|
const { user: currentUser, hasPermission } = useUser();
|
||||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||||
const [currentSortDirection, setCurrentSortDirection] =
|
const [currentSortDirection, setCurrentSortDirection] =
|
||||||
@@ -62,6 +64,13 @@ const RequestList = () => {
|
|||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||||
|
|
||||||
|
const { data: radarrData } = useSWR<RadarrSettings[]>(
|
||||||
|
hasPermission(Permission.ADMIN) ? '/api/v1/settings/radarr' : null
|
||||||
|
);
|
||||||
|
const { data: sonarrData } = useSWR<SonarrSettings[]>(
|
||||||
|
hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
@@ -245,6 +254,8 @@ const RequestList = () => {
|
|||||||
<RequestItem
|
<RequestItem
|
||||||
request={request}
|
request={request}
|
||||||
revalidateList={() => revalidate()}
|
revalidateList={() => revalidate()}
|
||||||
|
radarrData={radarrData}
|
||||||
|
sonarrData={sonarrData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ const TvRequestModal = ({
|
|||||||
mediaType: 'tv',
|
mediaType: 'tv',
|
||||||
is4k,
|
is4k,
|
||||||
seasons: settings.currentSettings.partialRequestsEnabled
|
seasons: settings.currentSettings.partialRequestsEnabled
|
||||||
? selectedSeasons
|
? selectedSeasons.sort((a, b) => a - b)
|
||||||
: getAllSeasons().filter(
|
: getAllSeasons().filter(
|
||||||
(season) => !getAllRequestedSeasons().includes(season)
|
(season) => !getAllRequestedSeasons().includes(season)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ const NotificationsEmail = () => {
|
|||||||
requireTls: values.encryption === 'opportunistic',
|
requireTls: values.encryption === 'opportunistic',
|
||||||
authUser: values.authUser,
|
authUser: values.authUser,
|
||||||
authPass: values.authPass,
|
authPass: values.authPass,
|
||||||
|
allowSelfSigned: values.allowSelfSigned,
|
||||||
senderName: values.senderName,
|
senderName: values.senderName,
|
||||||
pgpPrivateKey: values.pgpPrivateKey,
|
pgpPrivateKey: values.pgpPrivateKey,
|
||||||
pgpPassword: values.pgpPassword,
|
pgpPassword: values.pgpPassword,
|
||||||
|
|||||||
@@ -373,11 +373,10 @@ const TitleCard = ({
|
|||||||
: intl.formatMessage(globalMessages.tvshow)}
|
: intl.formatMessage(globalMessages.tvshow)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showDetail &&
|
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
|
||||||
currentStatus !== MediaStatus.BLACKLISTED &&
|
<div className="flex flex-col gap-1">
|
||||||
user?.userType !== UserType.PLEX && (
|
{user?.userType !== UserType.PLEX &&
|
||||||
<div className="flex flex-col gap-1">
|
(toggleWatchlist ? (
|
||||||
{toggleWatchlist ? (
|
|
||||||
<Button
|
<Button
|
||||||
buttonType={'ghost'}
|
buttonType={'ghost'}
|
||||||
className="z-40"
|
className="z-40"
|
||||||
@@ -394,23 +393,23 @@ const TitleCard = ({
|
|||||||
>
|
>
|
||||||
<MinusCircleIcon className={'h-3'} />
|
<MinusCircleIcon className={'h-3'} />
|
||||||
</Button>
|
</Button>
|
||||||
|
))}
|
||||||
|
{showHideButton &&
|
||||||
|
currentStatus !== MediaStatus.PROCESSING &&
|
||||||
|
currentStatus !== MediaStatus.AVAILABLE &&
|
||||||
|
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
|
||||||
|
currentStatus !== MediaStatus.PENDING && (
|
||||||
|
<Button
|
||||||
|
buttonType={'ghost'}
|
||||||
|
className="z-40"
|
||||||
|
buttonSize={'sm'}
|
||||||
|
onClick={() => setShowBlacklistModal(true)}
|
||||||
|
>
|
||||||
|
<EyeSlashIcon className={'h-3'} />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
{showHideButton &&
|
</div>
|
||||||
currentStatus !== MediaStatus.PROCESSING &&
|
)}
|
||||||
currentStatus !== MediaStatus.AVAILABLE &&
|
|
||||||
currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
|
|
||||||
currentStatus !== MediaStatus.PENDING && (
|
|
||||||
<Button
|
|
||||||
buttonType={'ghost'}
|
|
||||||
className="z-40"
|
|
||||||
buttonSize={'sm'}
|
|
||||||
onClick={() => setShowBlacklistModal(true)}
|
|
||||||
>
|
|
||||||
<EyeSlashIcon className={'h-3'} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showDetail &&
|
{showDetail &&
|
||||||
showHideButton &&
|
showHideButton &&
|
||||||
currentStatus == MediaStatus.BLACKLISTED && (
|
currentStatus == MediaStatus.BLACKLISTED && (
|
||||||
|
|||||||
@@ -671,7 +671,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<PlayButton links={mediaLinks} />
|
<div className="z-20">
|
||||||
|
<PlayButton links={mediaLinks} />
|
||||||
|
</div>
|
||||||
<RequestButton
|
<RequestButton
|
||||||
mediaType="tv"
|
mediaType="tv"
|
||||||
onUpdate={() => revalidate()}
|
onUpdate={() => revalidate()}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ const UserGeneralSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-[22]">
|
||||||
<RegionSelector
|
<RegionSelector
|
||||||
name="discoverRegion"
|
name="discoverRegion"
|
||||||
value={values.discoverRegion ?? ''}
|
value={values.discoverRegion ?? ''}
|
||||||
@@ -433,7 +433,7 @@ const UserGeneralSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-[21]">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
serverValue={currentSettings.originalLanguage}
|
serverValue={currentSettings.originalLanguage}
|
||||||
@@ -451,7 +451,7 @@ const UserGeneralSettings = () => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
<div className="form-input-field">
|
<div className="form-input-field relative z-20">
|
||||||
<RegionSelector
|
<RegionSelector
|
||||||
name="streamingRegion"
|
name="streamingRegion"
|
||||||
value={values.streamingRegion || ''}
|
value={values.streamingRegion || ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user