mirror of
https://github.com/seerr-team/seerr.git
synced 2026-07-30 17:47:17 -04:00
Merge branch 'develop' into feature/user-search-lookup
This commit is contained in:
@@ -45,8 +45,10 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.tsx'],
|
||||
plugins: ['react'],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'react/self-closing-comp': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -129,7 +129,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build (per-arch, native runners)
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: publish
|
||||
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Determine Workflow Status
|
||||
|
||||
87
.github/workflows/create-tag.yml
vendored
Normal file
87
.github/workflows/create-tag.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Create tag
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
determine-tag-version:
|
||||
name: Determine tag version
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
tag_version: ${{ steps.git-cliff.outputs.tag_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install git-cliff
|
||||
uses: taiki-e/install-action@cede0bb282aae847dfa8aacca3a41c86d973d4d7 # v2.68.1
|
||||
with:
|
||||
tool: git-cliff
|
||||
|
||||
- name: Get tag version
|
||||
id: git-cliff
|
||||
run: |
|
||||
tag_version=$(git-cliff -c .github/cliff.toml --bumped-version --unreleased)
|
||||
echo "Next tag version is ${tag_version}"
|
||||
echo "tag_version=${tag_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
create-tag:
|
||||
name: Create tag
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
needs: determine-tag-version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_VERSION: ${{ needs.determine-tag-version.outputs.tag_version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
with:
|
||||
ssh-key: '${{ secrets.COMMIT_KEY }}'
|
||||
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
# For workflows with elevated privileges we recommend disabling automatic caching.
|
||||
# https://github.com/actions/setup-node
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.name "${{ github.actor }}"
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
|
||||
- name: Bump package.json
|
||||
run: npm version ${TAG_VERSION} --no-commit-hooks --no-git-tag-version
|
||||
|
||||
- name: Commit updated files
|
||||
run: |
|
||||
git add package.json
|
||||
git commit -m 'chore(release): prepare ${TAG_VERSION}'
|
||||
git push
|
||||
|
||||
- name: Create git tag
|
||||
run: |
|
||||
git tag ${TAG_VERSION}
|
||||
git push origin ${TAG_VERSION}
|
||||
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
@@ -304,42 +304,3 @@ jobs:
|
||||
run: gh release edit "${{ env.VERSION }}" --draft=false --repo "${{ github.repository }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: publish-release
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Determine status
|
||||
id: status
|
||||
run: |
|
||||
case "${{ needs.publish-release.result }}" in
|
||||
success) echo "status=Success" >> $GITHUB_OUTPUT; echo "colour=3066993" >> $GITHUB_OUTPUT ;;
|
||||
failure) echo "status=Failure" >> $GITHUB_OUTPUT; echo "colour=15158332" >> $GITHUB_OUTPUT ;;
|
||||
cancelled) echo "status=Cancelled" >> $GITHUB_OUTPUT; echo "colour=10181046" >> $GITHUB_OUTPUT ;;
|
||||
*) echo "status=Skipped" >> $GITHUB_OUTPUT; echo "colour=9807270" >> $GITHUB_OUTPUT ;;
|
||||
esac
|
||||
|
||||
- name: Send notification
|
||||
run: |
|
||||
WEBHOOK="${{ secrets.DISCORD_WEBHOOK }}"
|
||||
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"embeds": [{
|
||||
"title": "${{ steps.status.outputs.status }}: ${{ github.workflow }}",
|
||||
"color": ${{ steps.status.outputs.colour }},
|
||||
"fields": [
|
||||
{ "name": "Repository", "value": "[${{ github.repository }}](${{ github.server_url }}/${{ github.repository }})", "inline": true },
|
||||
{ "name": "Ref", "value": "${{ github.ref }}", "inline": true },
|
||||
{ "name": "Event", "value": "${{ github.event_name }}", "inline": true },
|
||||
{ "name": "Triggered by", "value": "${{ github.actor }}", "inline": true },
|
||||
{ "name": "Workflow", "value": "[${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||
]
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
curl -sS -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "$WEBHOOK" || true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
[[ -n $HUSKY_BYPASS ]] || npx commitlint --edit $1
|
||||
[ -n "$HUSKY_BYPASS" ] || npx commitlint --edit $1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.next/
|
||||
dist/
|
||||
config/
|
||||
cache/config.json
|
||||
pnpm-lock.yaml
|
||||
cypress/config/settings.cypress.json
|
||||
.github
|
||||
@@ -17,4 +18,4 @@ public/*
|
||||
**/charts
|
||||
|
||||
# Prettier breaks GitHub alert syntax in markdown
|
||||
*.md
|
||||
*.md
|
||||
|
||||
@@ -33,5 +33,11 @@ module.exports = {
|
||||
rangeEnd: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: 'cache/config.json',
|
||||
options: {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,6 +6,12 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Automated AI-generated contributions without human review are not allowed and will be rejected.
|
||||
> This is an open-source project maintained by volunteers.
|
||||
> We do not have the resources to review pull requests that could have been avoided with proper human oversight.
|
||||
> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards.
|
||||
> Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban.
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Seerr,
|
||||
> it must be disclosed in the pull request.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ kubeVersion: '>=1.23.0-0'
|
||||
name: seerr-chart
|
||||
description: Seerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 3.1.0
|
||||
version: 3.2.0
|
||||
# renovate: image=ghcr.io/seerr-team/seerr
|
||||
appVersion: 'v3.0.1'
|
||||
maintainers:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# seerr-chart
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Seerr helm chart for Kubernetes
|
||||
|
||||
@@ -77,6 +77,18 @@ If `replicaCount` value was used - remove it. Helm update should work fine after
|
||||
| probes.readinessProbe | object | `{}` | Configure readiness probe |
|
||||
| probes.startupProbe | string | `nil` | Configure startup probe |
|
||||
| resources | object | `{}` | |
|
||||
| route.main.additionalRules | list | `[]` | |
|
||||
| route.main.annotations | object | `{}` | |
|
||||
| route.main.apiVersion | string | `"gateway.networking.k8s.io/v1"` | Set the route apiVersion, e.g. gateway.networking.k8s.io/v1 or gateway.networking.k8s.io/v1alpha2 |
|
||||
| route.main.enabled | bool | `false` | Enables or disables the Gateway API route |
|
||||
| route.main.filters | list | `[]` | |
|
||||
| route.main.hostnames | list | `[]` | |
|
||||
| route.main.httpsRedirect | bool | `false` | To redirect to HTTPS, create a new route object under the main route and enable this option. This should only be used with HTTP-like routes, such as HTTPRoute or GRPCRoute. [Reference]( https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/ ) |
|
||||
| route.main.kind | string | `"HTTPRoute"` | Set the route kind. Note that experimental kinds require changing `apiVersion` |
|
||||
| route.main.labels | object | `{}` | |
|
||||
| route.main.matches[0].path.type | string | `"PathPrefix"` | |
|
||||
| route.main.matches[0].path.value | string | `"/"` | |
|
||||
| route.main.parentRefs | list | `[]` | |
|
||||
| securityContext.allowPrivilegeEscalation | bool | `false` | |
|
||||
| securityContext.capabilities.drop[0] | string | `"ALL"` | |
|
||||
| securityContext.privileged | bool | `false` | |
|
||||
|
||||
50
charts/seerr-chart/templates/route.yaml
Normal file
50
charts/seerr-chart/templates/route.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
{{- range $name, $route := .Values.route }}
|
||||
{{- if $route.enabled }}
|
||||
---
|
||||
apiVersion: {{ $route.apiVersion | default "gateway.networking.k8s.io/v1" }}
|
||||
kind: {{ $route.kind | default "HTTPRoute" }}
|
||||
metadata:
|
||||
name: {{ template "seerr.fullname" $ }}{{ if ne $name "main" }}-{{ $name }}{{ end }}
|
||||
labels:
|
||||
{{- include "seerr.labels" $ | nindent 4 }}
|
||||
{{- with $route.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $route.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with $route.parentRefs }}
|
||||
parentRefs:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $route.hostnames }}
|
||||
hostnames:
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- with $route.additionalRules }}
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if $route.httpsRedirect }}
|
||||
- filters:
|
||||
- type: RequestRedirect
|
||||
requestRedirect:
|
||||
scheme: https
|
||||
statusCode: 301
|
||||
{{- else }}
|
||||
- backendRefs:
|
||||
- name: {{ include "seerr.fullname" $ }}
|
||||
port: {{ $.Values.service.port }}
|
||||
{{- with $route.filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $route.matches }}
|
||||
matches:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -105,6 +105,44 @@ ingress:
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
route:
|
||||
main:
|
||||
# -- Enables or disables the Gateway API route
|
||||
enabled: false
|
||||
|
||||
# -- Set the route apiVersion, e.g. gateway.networking.k8s.io/v1 or gateway.networking.k8s.io/v1alpha2
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
|
||||
# -- Set the route kind.
|
||||
# Note that experimental kinds require changing `apiVersion`
|
||||
kind: HTTPRoute
|
||||
|
||||
annotations: {}
|
||||
|
||||
labels: {}
|
||||
|
||||
parentRefs: []
|
||||
# - name: my-gateway
|
||||
# namespace: gateway
|
||||
# sectionName: https
|
||||
|
||||
hostnames: []
|
||||
# - seerr.example.com
|
||||
|
||||
additionalRules: []
|
||||
|
||||
filters: []
|
||||
|
||||
matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
|
||||
# -- To redirect to HTTPS, create a new route object under the main route and enable this option.
|
||||
# This should only be used with HTTP-like routes, such as HTTPRoute or GRPCRoute.
|
||||
# [Reference]( https://gateway-api.sigs.k8s.io/guides/http-redirect-rewrite/ )
|
||||
httpsRedirect: false
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
|
||||
@@ -30,7 +30,7 @@ If your PostgreSQL server is configured to accept TCP connections, you can speci
|
||||
|
||||
```dotenv
|
||||
DB_TYPE=postgres # Which DB engine to use, either sqlite or postgres. The default is sqlite.
|
||||
DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost".
|
||||
DB_HOST=localhost # (optional) The host (URL) of the database. The default is "localhost".
|
||||
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
||||
DB_USER= # (required) Username used to connect to the database.
|
||||
DB_PASS= # (required) Password of the user used to connect to the database.
|
||||
|
||||
111
docs/getting-started/third-parties/synology.mdx
Normal file
111
docs/getting-started/third-parties/synology.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Synology (Advanced)
|
||||
description: Install Seerr on Synology NAS using SynoCommunity
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Synology
|
||||
|
||||
:::warning
|
||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
This method is not recommended for most users. It is intended for advanced users who are using Synology NAS.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Synology NAS running **DSM 7.2** or later
|
||||
- 64-bit architecture (x86_64 or ARMv8)
|
||||
- [SynoCommunity package source](https://synocommunity.com/) added to Package Center
|
||||
|
||||
## Adding the SynoCommunity Package Source
|
||||
|
||||
If you haven't already added SynoCommunity to your Package Center:
|
||||
|
||||
1. Open **Package Center** in DSM
|
||||
2. Click **Settings** in the top-right corner
|
||||
3. Go to the **Package Sources** tab
|
||||
4. Click **Add**
|
||||
5. Enter the following:
|
||||
- **Name**: `SynoCommunity`
|
||||
- **Location**: `https://packages.synocommunity.com`
|
||||
6. Click **OK**
|
||||
|
||||
## Installation
|
||||
|
||||
1. In **Package Center**, search for **Seerr**
|
||||
2. Click **Install**
|
||||
3. Follow the installation wizard prompts
|
||||
4. Package Center will automatically install any required dependencies (Node.js v22)
|
||||
|
||||
### Access Seerr
|
||||
|
||||
Once installed, access Seerr at:
|
||||
|
||||
```
|
||||
http://<your-synology-ip>:5055
|
||||
```
|
||||
|
||||
You can also click the **Open** button in Package Center or find Seerr in the DSM main menu.
|
||||
|
||||
## Configuration
|
||||
|
||||
Seerr's configuration files are stored at:
|
||||
|
||||
```
|
||||
/var/packages/seerr/var/config
|
||||
```
|
||||
|
||||
:::info
|
||||
The Seerr package runs as a dedicated service user managed by DSM. No manual permission configuration is required.
|
||||
:::
|
||||
|
||||
## Managing the Service
|
||||
|
||||
You can start, stop, and restart Seerr from **Package Center** → Find Seerr → Use the action buttons.
|
||||
|
||||
## Updating
|
||||
|
||||
When a new version is available:
|
||||
|
||||
1. Open **Package Center**
|
||||
2. Go to **Installed** packages
|
||||
3. Find **Seerr** and click **Update** if available
|
||||
|
||||
:::tip
|
||||
Enable automatic updates in Package Center settings to keep Seerr up to date.
|
||||
:::
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
Seerr logs are located at `/var/packages/seerr/var/config/logs` and can be accessed using:
|
||||
|
||||
- **File Browser** package (recommended for most users)
|
||||
- SSH (advanced users)
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
Seerr uses port 5055. If this port is already in use:
|
||||
|
||||
- **Docker containers**: Remap the conflicting container to a different port
|
||||
- **Other packages**: The conflicting package will need to be uninstalled as Seerr's port cannot be changed
|
||||
|
||||
SynoCommunity ensures there are no port conflicts with other SynoCommunity packages or official Synology packages.
|
||||
|
||||
### Package Won't Start
|
||||
|
||||
Ensure Node.js v22 is installed and running by checking its status in **Package Center**.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
1. Open **Package Center**
|
||||
2. Find **Seerr** in your installed packages
|
||||
3. Click **Uninstall**
|
||||
|
||||
:::caution
|
||||
Uninstalling will remove the application but preserve your configuration data by default. Select "Remove data" during uninstallation if you want a complete removal.
|
||||
:::
|
||||
@@ -4,12 +4,6 @@ description: Install Seerr using TrueNAS
|
||||
sidebar_position: 4
|
||||
---
|
||||
# TrueNAS
|
||||
:::danger
|
||||
This method has not yet been updated for Seerr and is currently a work in progress.
|
||||
You can follow the ongoing work on this issue https://github.com/truenas/apps/issues/3374.
|
||||
:::
|
||||
|
||||
<!--
|
||||
:::warning
|
||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
||||
:::
|
||||
@@ -17,4 +11,7 @@ Third-party installation methods are maintained by the community. The Seerr team
|
||||
:::warning
|
||||
This method is not recommended for most users. It is intended for advanced users who are using TrueNAS distribution.
|
||||
:::
|
||||
-->
|
||||
|
||||
## Installation
|
||||
|
||||
Go to the 'Apps' menu, click the 'Discover Apps' button in the top right, search for 'Seerr' in the search bar, and install the app.
|
||||
|
||||
@@ -4,6 +4,9 @@ description: Install Seerr using Unraid
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Unraid
|
||||
|
||||
:::warning
|
||||
@@ -14,76 +17,160 @@ Third-party installation methods are maintained by the community. The Seerr team
|
||||
This method is not recommended for most users. It is intended for advanced users who are using Unraid.
|
||||
:::
|
||||
|
||||
## Template installation
|
||||
|
||||
If an official Unraid Community Applications template for Seerr isn't available in your catalog, you can install Seerr manually using Unraid's Docker UI.
|
||||
Several templates from Unraid Community are available right now
|
||||
- [Unraid Community Apps](https://unraid.net/community/apps#community-apps-iframe)
|
||||
|
||||
## Fresh Installation
|
||||
In case you still want to do it manually, please follow the instructions below.
|
||||
|
||||
### 1. Create the config directory
|
||||
## Manual Installation
|
||||
|
||||
:::note
|
||||
Seerr is now rootless. Unraid typically runs Docker containers as `nobody:users` (UID 99, GID 100), but Seerr now runs internally as UID 1000, GID 1000. This creates a permission mismatch.
|
||||
:::
|
||||
Before proceeding, choose which installation method best suits your server:
|
||||
|
||||
:::info
|
||||
**If migrating**: Copy your existing Jellyseerr/Overseerr config files (e.g., from `/mnt/user/appdata/overseerr/` or `/mnt/user/appdata/jellyseerr`) to `/mnt/user/appdata/seerr`, then apply the permissions below
|
||||
:::
|
||||
**Seerr Default** (`UID 1000:1000`) — Runs with the container's native `node` user. Matches the official image, but may break SMB share access, trigger "Fix Common Problems" warnings, and cause issues with backup plugins (such as CA Appdata Backup) and network integrations (such as Tailscale).
|
||||
|
||||
Open the Unraid terminal and run:
|
||||
**Unraid Default** (`UID 99:100`) — Runs with Unraid's native `nobody:users` ownership. Follows Unraid best practices, but overrides Seerr's built-in rootless user and may cause compatibility issues with future Seerr updates.
|
||||
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/seerr
|
||||
chown -R 1000:1000 /mnt/user/appdata/seerr
|
||||
```
|
||||
<Tabs groupId="unraid-permissions" queryString>
|
||||
<TabItem value="seerr-default" label="Seerr Default">
|
||||
|
||||
### 2. Add the Docker container
|
||||
### 1. Create the config directory
|
||||
|
||||
Navigate to the **Docker** tab in Unraid and click **Add Container**. Fill in the following:
|
||||
Open the Unraid terminal and run:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `seerr` |
|
||||
| **Repository** | `ghcr.io/seerr-team/seerr:latest` |
|
||||
| **Registry URL** (optional) | `https://ghcr.io` |
|
||||
| **Icon URL** | `https://raw.githubusercontent.com/seerr-team/seerr/develop/public/android-chrome-512x512.png` |
|
||||
| **WebUI** | `http://[IP]:[PORT:5055]` |
|
||||
| **Extra Parameters** | `--init` |
|
||||
| **Network Type** | `bridge` |
|
||||
| **Privileged** | `Off` |
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/seerr
|
||||
```
|
||||
|
||||
Then click **Add another Path, Port, Variable** to add:
|
||||
### 2. Set folder permissions
|
||||
|
||||
**Port:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Port | `5055` |
|
||||
| Host Port | `5055` |
|
||||
| Connection Type | `TCP` |
|
||||
```bash
|
||||
chown -R 1000:1000 /mnt/user/appdata/seerr
|
||||
```
|
||||
|
||||
**Path:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Path | `/app/config` |
|
||||
| Host Path | `/mnt/user/appdata/seerr` |
|
||||
### 3. Add the Docker container
|
||||
|
||||
**Variable:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `TZ` |
|
||||
| Value | Your [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`) |
|
||||
:::warning
|
||||
The **Extra Parameters** field is critical. You **must** add `--init --restart=unless-stopped` (see table below). Without `--init`, the container will not handle signals properly or shut down cleanly. Without `--restart=unless-stopped`, the container will not automatically restart after a reboot.
|
||||
|
||||
**Variable (optional):**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `LOG_LEVEL` |
|
||||
| Value | `info` |
|
||||
To see the **Extra Parameters** field, click **Basic View** in the top-right corner of the template page to switch to the advanced editor.
|
||||
:::
|
||||
|
||||
Click **Apply** to create and start the container.
|
||||
Navigate to the **Docker** tab in Unraid and click **Add Container**. Fill in the following:
|
||||
|
||||
### 3. Access Seerr
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `seerr` |
|
||||
| **Repository** | `ghcr.io/seerr-team/seerr:latest` |
|
||||
| **Icon URL** | `https://raw.githubusercontent.com/seerr-team/seerr/develop/public/android-chrome-512x512.png` |
|
||||
| **WebUI** | `http://[IP]:[PORT:5055]` |
|
||||
| **Extra Parameters** | `--init --restart=unless-stopped` |
|
||||
| **Network Type** | `bridge` |
|
||||
| **Privileged** | `Off` |
|
||||
|
||||
Open the WebUI at `http://<your-unraid-ip>:5055` and follow the setup wizard.
|
||||
Then click **Add another Path, Port, Variable** to add:
|
||||
|
||||
:::info
|
||||
The `--init` flag in **Extra Parameters** is required. Seerr does not include its own init process, so `--init` ensures proper signal handling and clean container shutdowns.
|
||||
:::
|
||||
**Port:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Port | `5055` |
|
||||
| Host Port | `5055` |
|
||||
| Connection Type | `TCP` |
|
||||
|
||||
**Path:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Path | `/app/config` |
|
||||
| Host Path | `/mnt/user/appdata/seerr` |
|
||||
|
||||
**Variable:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `TZ` |
|
||||
| Value | Your [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`) |
|
||||
|
||||
**Variable (optional):**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `LOG_LEVEL` |
|
||||
| Value | `info` |
|
||||
|
||||
Click **Apply** to create and start the container.
|
||||
|
||||
### 4. Access Seerr
|
||||
|
||||
Open the WebUI at `http://<your-unraid-ip>:5055` and follow the setup wizard.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="unraid-default" label="Unraid Default">
|
||||
|
||||
### 1. Create the config directory
|
||||
|
||||
Open the Unraid terminal and run:
|
||||
|
||||
```bash
|
||||
mkdir -p /mnt/user/appdata/seerr
|
||||
```
|
||||
|
||||
### 2. Set folder permissions
|
||||
|
||||
```bash
|
||||
chown -R 99:100 /mnt/user/appdata/seerr
|
||||
```
|
||||
|
||||
### 3. Add the Docker container
|
||||
|
||||
:::warning
|
||||
The **Extra Parameters** field is critical. You **must** add `--init --restart=unless-stopped --user 99:100` (see table below). Without `--init`, the container will not handle signals properly or shut down cleanly. Without `--restart=unless-stopped`, the container will not automatically restart after a reboot. The `--user 99:100` parameter runs the container process with Unraid's default UID/GID and avoids permission errors accessing your shares without changing host folder ownership.
|
||||
|
||||
To see the **Extra Parameters** field, click **Basic View** in the top-right corner of the template page to switch to the advanced editor.
|
||||
:::
|
||||
|
||||
Navigate to the **Docker** tab in Unraid and click **Add Container**. Fill in the following:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `seerr` |
|
||||
| **Repository** | `ghcr.io/seerr-team/seerr:latest` |
|
||||
| **Icon URL** | `https://raw.githubusercontent.com/seerr-team/seerr/develop/public/android-chrome-512x512.png` |
|
||||
| **WebUI** | `http://[IP]:[PORT:5055]` |
|
||||
| **Extra Parameters** | `--init --restart=unless-stopped --user 99:100` |
|
||||
| **Network Type** | `bridge` |
|
||||
| **Privileged** | `Off` |
|
||||
|
||||
Then click **Add another Path, Port, Variable** to add:
|
||||
|
||||
**Port:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Port | `5055` |
|
||||
| Host Port | `5055` |
|
||||
| Connection Type | `TCP` |
|
||||
|
||||
**Path:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Path | `/app/config` |
|
||||
| Host Path | `/mnt/user/appdata/seerr` |
|
||||
|
||||
**Variable:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `TZ` |
|
||||
| Value | Your [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`) |
|
||||
|
||||
**Variable (optional):**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `LOG_LEVEL` |
|
||||
| Value | `info` |
|
||||
|
||||
Click **Apply** to create and start the container.
|
||||
|
||||
### 4. Access Seerr
|
||||
|
||||
Open the WebUI at `http://<your-unraid-ip>:5055` and follow the setup wizard.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@@ -200,6 +200,11 @@ Summary of changes :
|
||||
</Tabs>
|
||||
|
||||
## Third-party installation methods
|
||||
|
||||
:::warning
|
||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
||||
:::
|
||||
|
||||
### Nix
|
||||
|
||||
Waiting for https://github.com/NixOS/nixpkgs/pull/450096 and https://github.com/NixOS/nixpkgs/pull/450093
|
||||
@@ -210,7 +215,42 @@ See https://aur.archlinux.org/packages/seerr
|
||||
|
||||
### TrueNAS
|
||||
|
||||
Waiting for https://github.com/truenas/apps/issues/3374
|
||||
Refer to [Seerr TrueNAS Documentation](/getting-started/third-parties/truenas), all of our examples have been updated to reflect the below change.
|
||||
|
||||
<Tabs groupId="truenas-migration" queryString>
|
||||
<TabItem value="hostpath" label="Host Path">
|
||||
**This guide describes how to migrate from Host Path storage (not ixVolume).**
|
||||
1. Stop Jellyseerr/Overseerr
|
||||
2. Install Seerr and use the same Host Path storage that was used by Jellyseerr/Overseerr
|
||||
3. Start Seerr app
|
||||
4. Delete Jellyseerr/Overseerr app
|
||||
</TabItem>
|
||||
<TabItem value="ixvolume" label="ixVolume">
|
||||
**This guide describes how to migrate from ixVolume storage (not Host Path).**
|
||||
1. Stop Jellyseerr/Overseerr
|
||||
2. Create a dataset for Seerr
|
||||
If your apps normally store data under something like:
|
||||
```
|
||||
/mnt/storage/<app-name>
|
||||
```
|
||||
then create a dataset named:
|
||||
```
|
||||
storage/seerr
|
||||
```
|
||||
resulting in:
|
||||
```
|
||||
/mnt/storage/seerr
|
||||
```
|
||||
3. Copy ixVolume Data
|
||||
Open System Settings → Shell, or SSH into your TrueNAS server as root and run :
|
||||
```bash
|
||||
rsync -av /mnt/.ix-apps/app_mounts/jellyseerr/ /mnt/storage/seerr/
|
||||
```
|
||||
4. Install Seerr and use the same Host Path storage that was created before (`/mnt/storage/seerr/config` in our example)
|
||||
5. Start Seerr app
|
||||
6. Delete Jellyseerr/Overseerr app
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Unraid
|
||||
|
||||
@@ -218,63 +258,23 @@ Refer to [Seerr Unraid Documentation](/getting-started/third-parties/unraid), al
|
||||
|
||||
Seerr will automatically migrate your existing Overseerr or Jellyseerr data on first startup. No manual database migration is needed.
|
||||
|
||||
1. Stop and remove the old Overseerr (or Jellyseerr) container from the Unraid **Docker** tab. Click the container icon, then **Stop**, then **Remove**. **⚠️ Do not delete the appdata folder ⚠️**
|
||||
**1. Stop the existing container**
|
||||
In the Unraid **Docker** tab, stop your Overseerr (or Jellyseerr) container.
|
||||
**⚠️ Do not remove the container or delete the appdata folder yet ⚠️**
|
||||
|
||||
2. Back up your existing appdata folder:
|
||||
**2. Copy existing data to Seerr appdata**
|
||||
Open the Unraid terminal and copy your existing appdata folder into the new Seerr appdata directory:
|
||||
```bash
|
||||
cp -a /mnt/user/appdata/overseerr /mnt/user/appdata/overseerr-backup
|
||||
cp -a /mnt/user/appdata/overseerr /mnt/user/appdata/seerr
|
||||
```
|
||||
|
||||
3. Fix config folder permissions — Seerr runs as the `node` user (UID 1000) instead of root:
|
||||
```bash
|
||||
chown -R 1000:1000 /mnt/user/appdata/overseerr
|
||||
```
|
||||
For Jellyseerr users, replace `overseerr` with `jellyseerr` in the path above.
|
||||
*(For Jellyseerr users, replace `overseerr` with `jellyseerr` in the paths).*
|
||||
|
||||
4. Add a new container in the Unraid **Docker** tab. Click **Add Container** and fill in the following:
|
||||
**3. Set permissions and install Seerr**
|
||||
Follow the [Unraid Installation Guide](/getting-started/third-parties/unraid#2-set-folder-permissions), **starting from step 2** — this covers setting the correct folder permissions and adding the Docker container. The guide offers two permission methods (**Seerr Default** and **Unraid Default**), each with trade-offs — read the descriptions before choosing.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `seerr` |
|
||||
| **Repository** | `ghcr.io/seerr-team/seerr:latest` |
|
||||
| **Registry URL** (optional) | `https://ghcr.io` |
|
||||
| **Icon URL** | `https://raw.githubusercontent.com/seerr-team/seerr/develop/public/android-chrome-512x512.png` |
|
||||
| **WebUI** | `http://[IP]:[PORT:5055]` |
|
||||
| **Extra Parameters** | `--init` |
|
||||
| **Network Type** | `bridge` |
|
||||
| **Privileged** | `Off` |
|
||||
**4. Start the new Seerr app**
|
||||
Start the newly created Seerr container. Check the container logs to confirm the automatic migration completed successfully.
|
||||
|
||||
Then click **Add another Path, Port, Variable** to add:
|
||||
|
||||
**Port:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Port | `5055` |
|
||||
| Host Port | `5055` |
|
||||
| Connection Type | `TCP` |
|
||||
|
||||
**Path** — point this to your existing config folder:
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Container Path | `/app/config` |
|
||||
| Host Path | `/mnt/user/appdata/overseerr` |
|
||||
|
||||
For Jellyseerr users, use `/mnt/user/appdata/jellyseerr`.
|
||||
|
||||
**Variable:**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `TZ` |
|
||||
| Value | Your [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`) |
|
||||
|
||||
**Variable (optional):**
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Key | `LOG_LEVEL` |
|
||||
| Value | `info` |
|
||||
|
||||
5. Click **Apply** to start the container. Check the container logs to confirm the automatic migration completed successfully.
|
||||
|
||||
:::tip
|
||||
If you are using a reverse proxy (such as SWAG or Nginx Proxy Manager), update your proxy configuration to point to the new container name `seerr`. The default port remains `5055`.
|
||||
:::
|
||||
**5. Remove the old app**
|
||||
Once you have confirmed Seerr is working properly and your data has successfully migrated, you can safely **Remove** the old Overseerr (or Jellyseerr) container from Unraid.
|
||||
@@ -19,7 +19,7 @@ Please check how to migrate to Seerr in our [migration guide](https://docs.seerr
|
||||
|
||||
Seerr brings several features that were previously available in Jellyseerr but missing from Overseerr. These additions improve flexibility, performance, and overall control for admins and power users:
|
||||
|
||||
* **Alternative media solution:** Added support for Jellyfin and Emby in addition to the existing Plex integration.
|
||||
* **Alternative media solution:** Added support for Jellyfin and Emby as alternatives to Plex. Only one integration can be used at a time.
|
||||
* **PostgreSQL support**: In addition to SQLite, you can now opt in to using a PostgreSQL database.
|
||||
* **Blocklist for movies, series, and tags**: Allows permitted users to hide movies, series, or tags from regular users.
|
||||
* **Override rules**: Adjust default request settings based on conditions such as user, tag, or other criteria.
|
||||
|
||||
@@ -16,7 +16,12 @@ const config: Config = {
|
||||
deploymentBranch: 'gh-pages',
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"postinstall": "next telemetry disable",
|
||||
"dev": "nodemon -e ts --watch server --watch seerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
|
||||
"build:next": "next build",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import type { DVRSettings } from '@server/lib/settings';
|
||||
import { getSettings, type DVRSettings } from '@server/lib/settings';
|
||||
|
||||
export interface SystemStatus {
|
||||
version: string;
|
||||
@@ -92,14 +92,14 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
||||
apiKey,
|
||||
cacheName,
|
||||
apiName,
|
||||
timeout = 10000,
|
||||
}: {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
cacheName: AvailableCacheIds;
|
||||
apiName: string;
|
||||
timeout?: number;
|
||||
}) {
|
||||
const timeout = getSettings().network.apiRequestTimeout;
|
||||
|
||||
super(
|
||||
url,
|
||||
{
|
||||
|
||||
@@ -64,16 +64,8 @@ export interface RadarrMovie {
|
||||
}
|
||||
|
||||
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
constructor({
|
||||
url,
|
||||
apiKey,
|
||||
timeout,
|
||||
}: {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr', timeout });
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr' });
|
||||
}
|
||||
|
||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||
@@ -184,10 +176,27 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
}
|
||||
|
||||
if (movie.id) {
|
||||
logger.info(
|
||||
'Movie is already monitored in Radarr. Skipping add and returning success',
|
||||
{ label: 'Radarr' }
|
||||
);
|
||||
// Movie exists and is already monitored
|
||||
logger.info('Movie is already monitored in Radarr.', {
|
||||
label: 'Radarr',
|
||||
movieId: movie.id,
|
||||
movieTitle: movie.title,
|
||||
hasFile: movie.hasFile,
|
||||
});
|
||||
|
||||
// If searchNow is requested and movie doesn't have a file, trigger search
|
||||
if (options.searchNow && !movie.hasFile) {
|
||||
logger.info(
|
||||
'Triggering search for existing monitored movie without file',
|
||||
{
|
||||
label: 'Radarr',
|
||||
movieId: movie.id,
|
||||
movieTitle: movie.title,
|
||||
}
|
||||
);
|
||||
this.searchMovie(movie.id);
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,16 +111,8 @@ class SonarrAPI extends ServarrBase<{
|
||||
episodeId: number;
|
||||
episode: EpisodeResult;
|
||||
}> {
|
||||
constructor({
|
||||
url,
|
||||
apiKey,
|
||||
timeout,
|
||||
}: {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
timeout?: number;
|
||||
}) {
|
||||
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr', timeout });
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' });
|
||||
}
|
||||
|
||||
public async getSeries(): Promise<SonarrSeries[]> {
|
||||
|
||||
@@ -206,6 +206,19 @@ class Media {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
public resetServiceData(): void {
|
||||
this.serviceId = null;
|
||||
this.serviceId4k = null;
|
||||
this.externalServiceId = null;
|
||||
this.externalServiceId4k = null;
|
||||
this.externalServiceSlug = null;
|
||||
this.externalServiceSlug4k = null;
|
||||
this.ratingKey = null;
|
||||
this.ratingKey4k = null;
|
||||
this.jellyfinMediaId = null;
|
||||
this.jellyfinMediaId4k = null;
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setPlexUrls(): void {
|
||||
const { machineId, webAppUrl } = getSettings().plex;
|
||||
|
||||
@@ -397,6 +397,13 @@ class JellyfinScanner
|
||||
episodes: totalStandard,
|
||||
episodes4k: total4k,
|
||||
});
|
||||
} else {
|
||||
processableSeasons.push({
|
||||
seasonNumber: season.season_number,
|
||||
totalEpisodes: season.episode_count,
|
||||
episodes: 0,
|
||||
episodes4k: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -430,6 +430,13 @@ class PlexScanner
|
||||
mediaIds.tmdbId = tmdbMedia.id;
|
||||
}
|
||||
|
||||
if (mediaIds.tvdbId && !mediaIds.tmdbId) {
|
||||
const show = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: mediaIds.tvdbId,
|
||||
});
|
||||
mediaIds.tmdbId = show.id;
|
||||
}
|
||||
|
||||
// Cache GUIDs
|
||||
guidCache.data.set(plexitem.ratingKey, mediaIds);
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { getMetadataProvider } from '@server/api/metadata';
|
||||
import type { SonarrSeries } from '@server/api/servarr/sonarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import type {
|
||||
TmdbKeyword,
|
||||
TmdbTvDetails,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import type {
|
||||
@@ -102,6 +108,15 @@ class SonarrScanner
|
||||
}
|
||||
|
||||
const tmdbId = tvShow.id;
|
||||
const metadataProvider = tvShow.keywords.results.some(
|
||||
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)
|
||||
? await getMetadataProvider('anime')
|
||||
: await getMetadataProvider('tv');
|
||||
|
||||
if (!(metadataProvider instanceof TheMovieDb)) {
|
||||
tvShow = await metadataProvider.getTvShow({ tvId: tmdbId });
|
||||
}
|
||||
const settings = getSettings();
|
||||
|
||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||
|
||||
@@ -171,6 +171,7 @@ export interface NetworkSettings {
|
||||
trustProxy: boolean;
|
||||
proxy: ProxySettings;
|
||||
dnsCache: DnsCacheSettings;
|
||||
apiRequestTimeout: number;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -593,6 +594,7 @@ class Settings {
|
||||
forceMinTtl: 0,
|
||||
forceMaxTtl: -1,
|
||||
},
|
||||
apiRequestTimeout: 10000,
|
||||
},
|
||||
migrations: [],
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ class WatchlistSync {
|
||||
[
|
||||
Permission.AUTO_REQUEST,
|
||||
Permission.AUTO_REQUEST_MOVIE,
|
||||
Permission.AUTO_APPROVE_TV,
|
||||
Permission.AUTO_REQUEST_TV,
|
||||
],
|
||||
{ type: 'or' }
|
||||
)
|
||||
@@ -70,13 +70,33 @@ class WatchlistSync {
|
||||
response.items.map((i) => i.tmdbId)
|
||||
);
|
||||
|
||||
const watchlistTmdbIds = response.items.map((i) => i.tmdbId);
|
||||
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const existingAutoRequests = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('request.requestedBy = :userId', { userId: user.id })
|
||||
.andWhere('request.isAutoRequest = true')
|
||||
.andWhere('media.tmdbId IN (:...tmdbIds)', { tmdbIds: watchlistTmdbIds })
|
||||
.getMany();
|
||||
|
||||
const autoRequestedTmdbIds = new Set(
|
||||
existingAutoRequests
|
||||
.filter((r) => r.media != null)
|
||||
.map((r) => `${r.media.mediaType}:${r.media.tmdbId}`)
|
||||
);
|
||||
|
||||
const unavailableItems = response.items.filter(
|
||||
// If we can find watchlist items in our database that are also available, we should exclude them
|
||||
(i) =>
|
||||
!autoRequestedTmdbIds.has(
|
||||
`${i.type === 'show' ? MediaType.TV : MediaType.MOVIE}:${i.tmdbId}`
|
||||
) &&
|
||||
!mediaItems.find(
|
||||
(m) =>
|
||||
m.tmdbId === i.tmdbId &&
|
||||
((m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
|
||||
(m.status === MediaStatus.BLOCKLISTED ||
|
||||
(m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
|
||||
(m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE))
|
||||
)
|
||||
);
|
||||
|
||||
@@ -174,7 +174,12 @@ mediaRoutes.delete(
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
await mediaRepository.remove(media);
|
||||
if (media.status === MediaStatus.BLOCKLISTED) {
|
||||
media.resetServiceData();
|
||||
await mediaRepository.save(media);
|
||||
} else {
|
||||
await mediaRepository.remove(media);
|
||||
}
|
||||
|
||||
return res.status(204).send();
|
||||
} catch (e) {
|
||||
@@ -310,12 +315,12 @@ mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
|
||||
if (media.ratingKey) {
|
||||
const watchStats = await tautulli.getMediaWatchStats(media.ratingKey);
|
||||
const watchUsers = await tautulli.getMediaWatchUsers(media.ratingKey);
|
||||
const plexIds = watchUsers.map((u) => u.user_id);
|
||||
if (!plexIds.length) plexIds.push(-1);
|
||||
|
||||
const users = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.plexId IN (:...plexIds)', {
|
||||
plexIds: watchUsers.map((u) => u.user_id),
|
||||
})
|
||||
.where('user.plexId IN (:...plexIds)', { plexIds })
|
||||
.getMany();
|
||||
|
||||
const playCount =
|
||||
@@ -342,12 +347,12 @@ mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
|
||||
const watchUsers4k = await tautulli.getMediaWatchUsers(
|
||||
media.ratingKey4k
|
||||
);
|
||||
const plexIds4k = watchUsers4k.map((u) => u.user_id);
|
||||
if (!plexIds4k.length) plexIds4k.push(-1);
|
||||
|
||||
const users = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.plexId IN (:...plexIds)', {
|
||||
plexIds: watchUsers4k.map((u) => u.user_id),
|
||||
})
|
||||
.where('user.plexId IN (:...plexIds)', { plexIds: plexIds4k })
|
||||
.getMany();
|
||||
|
||||
const playCount =
|
||||
|
||||
@@ -494,13 +494,16 @@ settingsRoutes.get(
|
||||
thumb: string;
|
||||
}[] = [];
|
||||
|
||||
const plexIds = plexUsers.map((plexUser) => plexUser.id);
|
||||
const plexEmails = plexUsers.map((plexUser) =>
|
||||
plexUser.email.toLowerCase()
|
||||
);
|
||||
if (!plexIds.length) plexIds.push('-1');
|
||||
if (!plexEmails.length) plexEmails.push('@');
|
||||
|
||||
const existingUsers = await qb
|
||||
.where('user.plexId IN (:...plexIds)', {
|
||||
plexIds: plexUsers.map((plexUser) => plexUser.id),
|
||||
})
|
||||
.orWhere('user.email IN (:...plexEmails)', {
|
||||
plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()),
|
||||
})
|
||||
.where('user.plexId IN (:...plexIds)', { plexIds })
|
||||
.orWhere('user.email IN (:...plexEmails)', { plexEmails })
|
||||
.getMany();
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -28,7 +28,7 @@ const AppDataWarning = () => {
|
||||
<Alert
|
||||
title={intl.formatMessage(messages.dockerVolumeMissingDescription, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
<code className="bg-gray-800/50">{msg}</code>
|
||||
),
|
||||
appDataPath: data.appDataPath,
|
||||
})}
|
||||
|
||||
@@ -441,13 +441,13 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => {
|
||||
)}
|
||||
<div className="card-field">
|
||||
{item.mediaType === 'movie' ? (
|
||||
<div className="pointer-events-none z-40 self-start rounded-full border border-blue-500 bg-blue-600 bg-opacity-80 shadow-md">
|
||||
<div className="pointer-events-none z-40 self-start rounded-full border border-blue-500 bg-blue-600/80 shadow-md">
|
||||
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
||||
{intl.formatMessage(globalMessages.movie)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pointer-events-none z-40 self-start rounded-full border border-purple-600 bg-purple-600 bg-opacity-80 shadow-md">
|
||||
<div className="pointer-events-none z-40 self-start rounded-full border border-purple-600 bg-purple-600/80 shadow-md">
|
||||
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
||||
{intl.formatMessage(globalMessages.tvshow)}
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,7 @@ interface AlertProps {
|
||||
|
||||
const Alert = ({ title, children, type }: AlertProps) => {
|
||||
let design = {
|
||||
bgColor:
|
||||
'border border-yellow-500 backdrop-blur bg-yellow-400 bg-opacity-20',
|
||||
bgColor: 'border border-yellow-500 backdrop-blur bg-yellow-400/20',
|
||||
titleColor: 'text-yellow-100',
|
||||
textColor: 'text-yellow-300',
|
||||
svg: <ExclamationTriangleIcon className="h-5 w-5" />,
|
||||
@@ -22,8 +21,7 @@ const Alert = ({ title, children, type }: AlertProps) => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
design = {
|
||||
bgColor:
|
||||
'border border-indigo-500 backdrop-blur bg-indigo-400 bg-opacity-20',
|
||||
bgColor: 'border border-indigo-500 backdrop-blur bg-indigo-400/20',
|
||||
titleColor: 'text-gray-100',
|
||||
textColor: 'text-gray-300',
|
||||
svg: <InformationCircleIcon className="h-5 w-5" />,
|
||||
|
||||
@@ -31,27 +31,25 @@ const Badge = (
|
||||
|
||||
switch (badgeType) {
|
||||
case 'danger':
|
||||
badgeStyle.push(
|
||||
'bg-red-600 bg-opacity-80 border-red-500 border !text-red-100'
|
||||
);
|
||||
badgeStyle.push('bg-red-600/80 border-red-500 border !text-red-100');
|
||||
if (href) {
|
||||
badgeStyle.push('hover:bg-red-500 bg-opacity-100');
|
||||
badgeStyle.push('hover:bg-red-500');
|
||||
}
|
||||
break;
|
||||
case 'warning':
|
||||
badgeStyle.push(
|
||||
'bg-yellow-500 bg-opacity-80 border-yellow-500 border !text-yellow-100'
|
||||
'bg-yellow-500/80 border-yellow-500 border !text-yellow-100'
|
||||
);
|
||||
if (href) {
|
||||
badgeStyle.push('hover:bg-yellow-500 hover:bg-opacity-100');
|
||||
badgeStyle.push('hover:bg-yellow-500');
|
||||
}
|
||||
break;
|
||||
case 'success':
|
||||
badgeStyle.push(
|
||||
'bg-green-500 bg-opacity-80 border border-green-500 !text-green-100'
|
||||
'bg-green-500/80 border border-green-500 !text-green-100'
|
||||
);
|
||||
if (href) {
|
||||
badgeStyle.push('hover:bg-green-500 hover:bg-opacity-100');
|
||||
badgeStyle.push('hover:bg-green-500');
|
||||
}
|
||||
break;
|
||||
case 'dark':
|
||||
@@ -68,10 +66,10 @@ const Badge = (
|
||||
break;
|
||||
default:
|
||||
badgeStyle.push(
|
||||
'bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100'
|
||||
'bg-indigo-500/80 border border-indigo-500 !text-indigo-100'
|
||||
);
|
||||
if (href) {
|
||||
badgeStyle.push('hover:bg-indigo-500 hover:bg-opacity-100');
|
||||
badgeStyle.push('hover:bg-indigo-500');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,22 +52,22 @@ function Button<P extends ElementTypes = 'button'>(
|
||||
switch (buttonType) {
|
||||
case 'primary':
|
||||
buttonStyle.push(
|
||||
'text-white border border-indigo-500 bg-indigo-600 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-opacity-100 active:border-indigo-700'
|
||||
'text-white border border-indigo-500 bg-indigo-600/80 hover:bg-indigo-600 hover:border-indigo-500 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-600 active:border-indigo-700'
|
||||
);
|
||||
break;
|
||||
case 'danger':
|
||||
buttonStyle.push(
|
||||
'text-white bg-red-600 bg-opacity-80 border-red-500 hover:bg-opacity-100 hover:border-red-500 focus:border-red-700 focus:ring-red active:bg-red-700 active:border-red-700'
|
||||
'text-white bg-red-600/80 border-red-500 hover:bg-red-600 hover:border-red-500 focus:border-red-700 focus:ring-red active:bg-red-700 active:border-red-700'
|
||||
);
|
||||
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 bg-yellow-500/80 hover:bg-yellow-500 hover:border-yellow-400 focus:border-yellow-700 focus:ring-yellow active:bg-yellow-500 active:border-yellow-700'
|
||||
);
|
||||
break;
|
||||
case 'success':
|
||||
buttonStyle.push(
|
||||
'text-white bg-green-500 bg-opacity-80 border-green-500 hover:bg-opacity-100 hover:border-green-400 focus:border-green-700 focus:ring-green active:bg-opacity-100 active:border-green-700'
|
||||
'text-white bg-green-500/80 border-green-500 hover:bg-green-500 hover:border-green-400 focus:border-green-700 focus:ring-green active:bg-green-500 active:border-green-700'
|
||||
);
|
||||
break;
|
||||
case 'ghost':
|
||||
@@ -77,7 +77,7 @@ function Button<P extends ElementTypes = 'button'>(
|
||||
break;
|
||||
default:
|
||||
buttonStyle.push(
|
||||
'text-gray-200 bg-gray-800 bg-opacity-80 border-gray-600 hover:text-white hover:bg-gray-700 hover:border-gray-600 group-hover:text-white group-hover:bg-gray-700 group-hover:border-gray-600 focus:border-blue-300 focus:ring-blue active:text-gray-200 active:bg-gray-700 active:border-gray-600'
|
||||
'text-gray-200 bg-gray-800/80 border-gray-600 hover:text-white hover:bg-gray-700 hover:border-gray-600 group-hover:text-white group-hover:bg-gray-700 group-hover:border-gray-600 focus:border-blue-300 focus:ring-blue active:text-gray-200 active:bg-gray-700 active:border-gray-600'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ const ButtonWithDropdown = ({
|
||||
break;
|
||||
default:
|
||||
styleClasses.mainButtonClasses +=
|
||||
' bg-indigo-600 border-indigo-500 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue';
|
||||
' bg-indigo-600/80 border-indigo-500 hover:bg-indigo-600 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue';
|
||||
styleClasses.dropdownSideButtonClasses +=
|
||||
' bg-indigo-600 bg-opacity-80 border-indigo-500 hover:bg-opacity-100 active:bg-opacity-100 focus:ring-blue';
|
||||
' bg-indigo-600/80 border-indigo-500 hover:bg-indigo-600 active:bg-indigo-600 focus:ring-blue';
|
||||
}
|
||||
|
||||
const TriggerElement = props.as ?? 'button';
|
||||
|
||||
@@ -59,7 +59,7 @@ const DropdownItems = ({
|
||||
className={[
|
||||
'absolute right-0 z-40 -mr-1 mt-2 w-56 origin-top-right rounded-md p-1 shadow-lg',
|
||||
dropdownType === 'ghost'
|
||||
? 'border border-gray-700 bg-gray-800 bg-opacity-80 backdrop-blur'
|
||||
? 'border border-gray-700 bg-gray-800/80 backdrop-blur'
|
||||
: 'bg-indigo-600',
|
||||
className,
|
||||
].join(' ')}
|
||||
@@ -95,7 +95,7 @@ const Dropdown = ({
|
||||
'button-md inline-flex h-full items-center space-x-2 rounded-md border px-4 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none',
|
||||
buttonType === 'ghost'
|
||||
? 'border-gray-600 bg-transparent hover:border-gray-200 focus:border-gray-100 active:border-gray-100'
|
||||
: 'focus:ring-blue border-indigo-500 bg-indigo-600 bg-opacity-80 hover:border-indigo-500 hover:bg-opacity-100 active:border-indigo-700 active:bg-indigo-700',
|
||||
: `focus:ring-blue border-indigo-500 bg-indigo-600/80 hover:border-indigo-500 hover:bg-indigo-600 active:border-indigo-700 active:bg-indigo-700`,
|
||||
className,
|
||||
].join(' ')}
|
||||
ref={buttonRef}
|
||||
|
||||
@@ -91,7 +91,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
<Transition.Child
|
||||
appear
|
||||
as="div"
|
||||
className="fixed bottom-0 left-0 right-0 top-0 z-50 flex h-full w-full items-center justify-center bg-gray-800 bg-opacity-70"
|
||||
className="fixed bottom-0 left-0 right-0 top-0 z-50 flex h-full w-full items-center justify-center bg-gray-800/70"
|
||||
enter="transition-opacity duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
|
||||
@@ -24,13 +24,13 @@ const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => {
|
||||
className={`${
|
||||
checked ? 'bg-indigo-500' : 'bg-gray-700'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
checked ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const SlideOver = ({
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={`fixed inset-0 z-50 overflow-hidden bg-gray-800 bg-opacity-70`}
|
||||
className={`fixed inset-0 z-50 overflow-hidden bg-gray-800/70`}
|
||||
onClick={() => onClose()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -71,7 +71,7 @@ const SlideOver = ({
|
||||
ref={slideoverRef}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex h-full flex-col rounded-lg bg-gray-800 bg-opacity-80 shadow-xl ring-1 ring-gray-700 backdrop-blur">
|
||||
<div className="flex h-full flex-col rounded-lg bg-gray-800/80 shadow-xl ring-1 ring-gray-700 backdrop-blur">
|
||||
<header className="space-y-1 border-b border-gray-700 px-4 py-4">
|
||||
<div className="flex items-center justify-between space-x-3">
|
||||
<h2 className="text-overseerr text-2xl font-bold leading-7">
|
||||
|
||||
@@ -24,7 +24,7 @@ const StatusBadgeMini = ({
|
||||
shrink = false,
|
||||
}: StatusBadgeMiniProps) => {
|
||||
const badgeStyle = [
|
||||
`rounded-full bg-opacity-80 shadow-md ${
|
||||
`rounded-full shadow-md ${
|
||||
shrink ? 'w-4 sm:w-5 border p-0' : 'w-5 ring-1 p-0.5'
|
||||
}`,
|
||||
];
|
||||
@@ -34,34 +34,34 @@ const StatusBadgeMini = ({
|
||||
switch (status) {
|
||||
case MediaStatus.PROCESSING:
|
||||
badgeStyle.push(
|
||||
'bg-indigo-500 border-indigo-400 ring-indigo-400 text-indigo-100'
|
||||
'bg-indigo-500/80 border-indigo-400 ring-indigo-400 text-indigo-100'
|
||||
);
|
||||
indicatorIcon = <ClockIcon />;
|
||||
break;
|
||||
case MediaStatus.AVAILABLE:
|
||||
badgeStyle.push(
|
||||
'bg-green-500 border-green-400 ring-green-400 text-green-100'
|
||||
'bg-green-500/80 border-green-400 ring-green-400 text-green-100'
|
||||
);
|
||||
indicatorIcon = <CheckCircleIcon />;
|
||||
break;
|
||||
case MediaStatus.PENDING:
|
||||
badgeStyle.push(
|
||||
'bg-yellow-500 border-yellow-400 ring-yellow-400 text-yellow-100'
|
||||
'bg-yellow-500/80 border-yellow-400 ring-yellow-400 text-yellow-100'
|
||||
);
|
||||
indicatorIcon = <BellIcon />;
|
||||
break;
|
||||
case MediaStatus.BLOCKLISTED:
|
||||
badgeStyle.push('bg-red-500 border-white-400 ring-white-400 text-white');
|
||||
badgeStyle.push('bg-red-500/80 border-white ring-white text-white');
|
||||
indicatorIcon = <EyeSlashIcon />;
|
||||
break;
|
||||
case MediaStatus.PARTIALLY_AVAILABLE:
|
||||
badgeStyle.push(
|
||||
'bg-green-500 border-green-400 ring-green-400 text-green-100'
|
||||
'bg-green-500/80 border-green-400 ring-green-400 text-green-100'
|
||||
);
|
||||
indicatorIcon = <MinusSmallIcon />;
|
||||
break;
|
||||
case MediaStatus.DELETED:
|
||||
badgeStyle.push('bg-red-500 border-red-400 ring-red-400 text-red-100');
|
||||
badgeStyle.push('bg-red-500/80 border-red-400 ring-red-400 text-red-100');
|
||||
indicatorIcon = <TrashIcon />;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||
typeof errors.data === 'string' && (
|
||||
<div className="error">{errors.data}</div>
|
||||
)}
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex-1" />
|
||||
{resultCount === 0 ? (
|
||||
<Tooltip content={intl.formatMessage(messages.needresults)}>
|
||||
<div>
|
||||
|
||||
@@ -159,7 +159,7 @@ const Discover = () => {
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
data-testid="discover-start-editing"
|
||||
className="h-12 w-12 rounded-full border-2 border-gray-600 bg-gray-700 bg-opacity-90 p-3 text-gray-400 shadow transition-all hover:bg-opacity-100"
|
||||
className="h-12 w-12 rounded-full border-2 border-gray-600 bg-gray-700/90 p-3 text-gray-400 shadow transition-all hover:bg-gray-700"
|
||||
>
|
||||
<PencilIcon className="h-full w-full" />
|
||||
</button>
|
||||
@@ -172,7 +172,7 @@ const Discover = () => {
|
||||
leave="transition duration-300"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-6"
|
||||
className="safe-shift-edit-menu fixed left-0 right-0 z-50 flex flex-col items-center justify-end space-x-0 space-y-2 border-t border-gray-700 bg-gray-800 bg-opacity-80 p-4 backdrop-blur sm:bottom-0 sm:flex-row sm:space-x-3 sm:space-y-0"
|
||||
className="safe-shift-edit-menu fixed left-0 right-0 z-50 flex flex-col items-center justify-end space-x-0 space-y-2 border-t border-gray-700 bg-gray-800/80 p-4 backdrop-blur sm:bottom-0 sm:flex-row sm:space-x-3 sm:space-y-0"
|
||||
>
|
||||
<Button
|
||||
buttonType="default"
|
||||
|
||||
@@ -20,8 +20,8 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
|
||||
canExpand ? 'w-full' : 'w-56 sm:w-72'
|
||||
} transform-gpu cursor-pointer p-8 shadow ring-1 transition duration-300 ease-in-out ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 bg-opacity-100 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 bg-opacity-80 ring-gray-700'
|
||||
? 'scale-105 bg-gray-700/100 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800/80 ring-gray-700'
|
||||
} overflow-hidden rounded-xl bg-cover bg-center`}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
@@ -43,8 +43,8 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 z-10 h-full w-full bg-gray-800 transition duration-300 ${
|
||||
isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
|
||||
className={`absolute inset-0 z-10 h-full w-full transition duration-300 ${
|
||||
isHovered ? 'bg-gray-800/10' : 'bg-gray-800/30'
|
||||
}`}
|
||||
/>
|
||||
<div className="relative z-20 w-full truncate whitespace-normal text-center text-2xl font-bold text-white sm:text-3xl">
|
||||
@@ -58,7 +58,7 @@ const GenreCardPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
className={`relative h-32 w-56 animate-pulse rounded-xl bg-gray-700 sm:h-40 sm:w-72`}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ const CreateIssueModal = ({
|
||||
<RadioGroup.Label className="sr-only">
|
||||
Select an Issue
|
||||
</RadioGroup.Label>
|
||||
<div className="-space-y-px overflow-hidden rounded-md bg-gray-800 bg-opacity-30">
|
||||
<div className="-space-y-px overflow-hidden rounded-md bg-gray-800/30">
|
||||
{issueOptions.map((setting, index) => (
|
||||
<RadioGroup.Option
|
||||
key={`issue-type-${setting.issueType}`}
|
||||
@@ -256,7 +256,7 @@ const CreateIssueModal = ({
|
||||
? 'rounded-bl-md rounded-br-md'
|
||||
: '',
|
||||
checked
|
||||
? 'z-10 border border-indigo-500 bg-indigo-400 bg-opacity-20'
|
||||
? 'z-10 border border-indigo-500 bg-indigo-400/20'
|
||||
: 'border-gray-500',
|
||||
'relative flex cursor-pointer border p-4 focus:outline-none'
|
||||
)
|
||||
|
||||
@@ -179,7 +179,7 @@ const MobileMenu = ({
|
||||
leave="transition duration-500"
|
||||
leaveFrom="opacity-100 -translate-y-full"
|
||||
leaveTo="opacity-0 translate-y-0"
|
||||
className="absolute left-0 right-0 top-0 flex w-full -translate-y-full flex-col space-y-6 border-t border-gray-600 bg-gray-900 bg-opacity-90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
|
||||
className="absolute left-0 right-0 top-0 flex w-full -translate-y-full flex-col space-y-6 border-t border-gray-600 bg-gray-900/90 px-6 py-6 font-semibold text-gray-100 backdrop-blur"
|
||||
>
|
||||
{filteredLinks.map((link) => {
|
||||
const isActive = router.pathname.match(link.activeRegExp);
|
||||
@@ -225,7 +225,7 @@ const MobileMenu = ({
|
||||
);
|
||||
})}
|
||||
</Transition>
|
||||
<div className="padding-bottom-safe border-t border-gray-600 bg-gray-800 bg-opacity-90 backdrop-blur">
|
||||
<div className="padding-bottom-safe border-t border-gray-600 bg-gray-800/90 backdrop-blur">
|
||||
<div className="flex h-full items-center justify-between px-6 py-4 text-gray-100">
|
||||
{filteredLinks
|
||||
.slice(0, filteredLinks.length === 5 ? 5 : 4)
|
||||
|
||||
@@ -24,7 +24,7 @@ const SearchInput = () => {
|
||||
<input
|
||||
id="search_field"
|
||||
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }}
|
||||
className="block w-full rounded-full border border-gray-600 bg-gray-900 bg-opacity-80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-opacity-100 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base"
|
||||
className="block w-full rounded-full border border-gray-600 bg-gray-900/80 py-2 pl-10 text-white placeholder-gray-300 hover:border-gray-500 focus:border-gray-500 focus:bg-gray-900 focus:placeholder-gray-400 focus:outline-none focus:ring-0 sm:text-base"
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
|
||||
@@ -163,7 +163,7 @@ const Sidebar = ({
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0">
|
||||
<div className="absolute inset-0 bg-gray-900 opacity-90"></div>
|
||||
<div className="absolute inset-0 bg-gray-900 opacity-90" />
|
||||
</div>
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
|
||||
@@ -74,7 +74,7 @@ const UserDropdown = () => {
|
||||
appear
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-72 origin-top-right rounded-md shadow-lg">
|
||||
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
|
||||
<div className="divide-y divide-gray-700 rounded-md bg-gray-800/80 ring-1 ring-gray-700 backdrop-blur">
|
||||
<div className="flex flex-col space-y-4 px-4 py-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CachedImage
|
||||
|
||||
@@ -88,8 +88,8 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
||||
<PullToRefresh />
|
||||
<div
|
||||
className={`searchbar fixed left-0 right-0 top-0 z-10 flex flex-shrink-0 bg-opacity-80 transition duration-300 ${
|
||||
isScrolled ? 'bg-gray-700' : 'bg-transparent'
|
||||
className={`searchbar fixed left-0 right-0 top-0 z-10 flex flex-shrink-0 transition duration-300 ${
|
||||
isScrolled ? 'bg-gray-700/80' : 'bg-transparent'
|
||||
} lg:left-64`}
|
||||
style={{
|
||||
backdropFilter: isScrolled ? 'blur(5px)' : undefined,
|
||||
|
||||
@@ -154,7 +154,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
{errors.password && touched.password && (
|
||||
<div className="error">{errors.password}</div>
|
||||
)}
|
||||
<div className="flex-grow"></div>
|
||||
<div className="flex-grow" />
|
||||
{baseUrl && (
|
||||
<a
|
||||
href={
|
||||
|
||||
@@ -120,7 +120,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
typeof errors.password === 'string' && (
|
||||
<div className="error">{errors.password}</div>
|
||||
)}
|
||||
<div className="flex-grow"></div>
|
||||
<div className="flex-grow" />
|
||||
{passwordResetEnabled && (
|
||||
<Link
|
||||
href="/resetpassword"
|
||||
|
||||
@@ -169,7 +169,7 @@ const Login = () => {
|
||||
</div>
|
||||
<div className="relative z-50 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div
|
||||
className="bg-gray-800 bg-opacity-50 shadow sm:rounded-lg"
|
||||
className="bg-gray-800/50 shadow sm:rounded-lg"
|
||||
style={{ backdropFilter: 'blur(5px)' }}
|
||||
>
|
||||
<>
|
||||
@@ -241,11 +241,11 @@ const Login = () => {
|
||||
{additionalLoginOptions.length > 0 &&
|
||||
(loginFormVisible ? (
|
||||
<div className="flex items-center py-5">
|
||||
<div className="flex-grow border-t border-gray-600"></div>
|
||||
<div className="flex-grow border-t border-gray-600" />
|
||||
<span className="mx-2 flex-shrink text-sm text-gray-400">
|
||||
{intl.formatMessage(messages.orsigninwith)}
|
||||
</span>
|
||||
<div className="flex-grow border-t border-gray-600"></div>
|
||||
<div className="flex-grow border-t border-gray-600" />
|
||||
</div>
|
||||
) : (
|
||||
<h2 className="mb-6 text-center text-lg font-bold text-neutral-200">
|
||||
|
||||
@@ -317,7 +317,7 @@ const CollectionRequestModal = ({
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-16 bg-gray-700 bg-opacity-80 px-4 py-3">
|
||||
<th className="w-16 bg-gray-700/80 px-4 py-3">
|
||||
<span
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
@@ -340,19 +340,19 @@ const CollectionRequestModal = ({
|
||||
className={`${
|
||||
isAllParts() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllParts() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
<th className="bg-gray-700 bg-opacity-80 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
<th className="bg-gray-700/80 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(globalMessages.movie)}
|
||||
</th>
|
||||
<th className="bg-gray-700 bg-opacity-80 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
<th className="bg-gray-700/80 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -423,7 +423,7 @@ const CollectionRequestModal = ({
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-700'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
@@ -435,7 +435,7 @@ const CollectionRequestModal = ({
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
|
||||
@@ -84,7 +84,7 @@ const SearchByNameModal = ({
|
||||
onClick={() => handleClick(item.tvdbId)}
|
||||
>
|
||||
<div
|
||||
className={`flex h-40 w-full items-center overflow-hidden rounded-xl border border-gray-700 bg-gray-700 bg-opacity-20 p-2 shadow backdrop-blur transition ${
|
||||
className={`flex h-40 w-full items-center overflow-hidden rounded-xl border border-gray-700 bg-gray-700/20 p-2 shadow backdrop-blur transition ${
|
||||
tvdbId === item.tvdbId ? 'ring ring-indigo-500' : ''
|
||||
} `}
|
||||
>
|
||||
|
||||
@@ -529,7 +529,7 @@ const TvRequestModal = ({
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className={`w-16 bg-gray-700 bg-opacity-80 px-4 py-3 ${
|
||||
className={`w-16 bg-gray-700/80 px-4 py-3 ${
|
||||
!settings.currentSettings.partialRequestsEnabled &&
|
||||
'hidden'
|
||||
}`}
|
||||
@@ -557,22 +557,22 @@ const TvRequestModal = ({
|
||||
className={`${
|
||||
isAllSeasons() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllSeasons() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
<th className="bg-gray-700 bg-opacity-80 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
<th className="bg-gray-700/80 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.season)}
|
||||
</th>
|
||||
<th className="bg-gray-700 bg-opacity-80 px-5 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
<th className="bg-gray-700/80 px-5 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.numberofepisodes)}
|
||||
</th>
|
||||
<th className="bg-gray-700 bg-opacity-80 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
<th className="bg-gray-700/80 px-2 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(globalMessages.status)}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -649,7 +649,7 @@ const TvRequestModal = ({
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-700'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
@@ -662,7 +662,7 @@ const TvRequestModal = ({
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
|
||||
@@ -65,7 +65,7 @@ const ResetPassword = () => {
|
||||
</div>
|
||||
<div className="relative z-50 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div
|
||||
className="bg-gray-800 bg-opacity-50 shadow sm:rounded-lg"
|
||||
className="bg-gray-800/50 shadow sm:rounded-lg"
|
||||
style={{ backdropFilter: 'blur(5px)' }}
|
||||
>
|
||||
<div className="px-10 py-8">
|
||||
|
||||
@@ -75,7 +75,7 @@ const ResetPassword = () => {
|
||||
</div>
|
||||
<div className="relative z-50 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div
|
||||
className="bg-gray-800 bg-opacity-50 shadow sm:rounded-lg"
|
||||
className="bg-gray-800/50 shadow sm:rounded-lg"
|
||||
style={{ backdropFilter: 'blur(5px)' }}
|
||||
>
|
||||
<div className="px-10 py-8">
|
||||
|
||||
@@ -228,7 +228,7 @@ const NotificationsTelegram = () => {
|
||||
</a>
|
||||
),
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
<code className="bg-gray-800/50">{msg}</code>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -60,7 +60,7 @@ const SettingsAbout = () => {
|
||||
intl.formatMessage(globalMessages.settings),
|
||||
]}
|
||||
/>
|
||||
<div className="mt-6 rounded-md border border-indigo-500 bg-indigo-400 bg-opacity-20 p-4 backdrop-blur">
|
||||
<div className="mt-6 rounded-md border border-indigo-500 bg-indigo-400/20 p-4 backdrop-blur">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon className="h-5 w-5 text-gray-100" />
|
||||
@@ -88,7 +88,7 @@ const SettingsAbout = () => {
|
||||
<Alert
|
||||
title={intl.formatMessage(messages.runningDevelop, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
<code className="bg-gray-800/50">{msg}</code>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -495,7 +495,7 @@ const SettingsJobs = () => {
|
||||
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.jobtype)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
<Table.TH />
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
@@ -578,7 +578,7 @@ const SettingsJobs = () => {
|
||||
<Table.TH>{intl.formatMessage(messages.cachekeys)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cacheksize)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.cachevsize)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
<Table.TH />
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
@@ -639,7 +639,7 @@ const SettingsJobs = () => {
|
||||
<Table.TH>
|
||||
{intl.formatMessage(messages.dnscacheage)}
|
||||
</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
<Table.TH />
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
@@ -744,7 +744,7 @@ const SettingsJobs = () => {
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.imagecacheDescription, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
<code className="bg-gray-800/50">{msg}</code>
|
||||
),
|
||||
appDataPath: appData ? appData.appDataPath : '/app/config',
|
||||
})}
|
||||
|
||||
@@ -245,7 +245,7 @@ const SettingsLogs = () => {
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.logsDescription, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="whitespace-normal break-words bg-opacity-50">
|
||||
<code className="whitespace-normal break-words bg-gray-800/50">
|
||||
{msg}
|
||||
</code>
|
||||
),
|
||||
@@ -314,7 +314,7 @@ const SettingsLogs = () => {
|
||||
<Table.TH>{intl.formatMessage(messages.level)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.label)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.message)}</Table.TH>
|
||||
<Table.TH></Table.TH>
|
||||
<Table.TH />
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
|
||||
@@ -56,6 +56,10 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
|
||||
'Do NOT enable this if you are experiencing issues with DNS lookups',
|
||||
dnsCacheForceMinTtl: 'DNS Cache Minimum TTL',
|
||||
dnsCacheForceMaxTtl: 'DNS Cache Maximum TTL',
|
||||
apiRequestTimeout: 'API Request Timeout',
|
||||
apiRequestTimeoutTip:
|
||||
'Maximum time (in seconds) to wait for responses from external services like Radarr/Sonarr. Set to 0 for no timeout.',
|
||||
validationApiRequestTimeout: 'You must provide a valid timeout value',
|
||||
});
|
||||
|
||||
const SettingsNetwork = () => {
|
||||
@@ -91,6 +95,10 @@ const SettingsNetwork = () => {
|
||||
.max(65535, intl.formatMessage(messages.validationProxyPort))
|
||||
.required(intl.formatMessage(messages.validationProxyPort)),
|
||||
}),
|
||||
apiRequestTimeout: Yup.number()
|
||||
.typeError(intl.formatMessage(messages.validationApiRequestTimeout))
|
||||
.required(intl.formatMessage(messages.validationApiRequestTimeout))
|
||||
.min(0, intl.formatMessage(messages.validationApiRequestTimeout)),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -130,6 +138,10 @@ const SettingsNetwork = () => {
|
||||
proxyPassword: data?.proxy?.password,
|
||||
proxyBypassFilter: data?.proxy?.bypassFilter,
|
||||
proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses,
|
||||
apiRequestTimeout:
|
||||
data?.apiRequestTimeout !== undefined
|
||||
? data.apiRequestTimeout / 1000
|
||||
: 10,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={NetworkSettingsSchema}
|
||||
@@ -154,6 +166,7 @@ const SettingsNetwork = () => {
|
||||
bypassFilter: values.proxyBypassFilter,
|
||||
bypassLocalAddresses: values.proxyBypassLocalAddresses,
|
||||
},
|
||||
apiRequestTimeout: Number(values.apiRequestTimeout) * 1000,
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
mutate('/api/v1/status');
|
||||
@@ -341,6 +354,31 @@ const SettingsNetwork = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<label htmlFor="apiRequestTimeout" className="text-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.apiRequestTimeout)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.apiRequestTimeoutTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
id="apiRequestTimeout"
|
||||
name="apiRequestTimeout"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className="short"
|
||||
/>
|
||||
</div>
|
||||
{errors.apiRequestTimeout &&
|
||||
touched.apiRequestTimeout &&
|
||||
typeof errors.apiRequestTimeout === 'string' && (
|
||||
<div className="error">{errors.apiRequestTimeout}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
|
||||
@@ -164,7 +164,7 @@ const Setup = () => {
|
||||
<AppDataWarning />
|
||||
<nav className="relative z-50">
|
||||
<ul
|
||||
className="divide-y divide-gray-600 rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 md:flex md:divide-y-0"
|
||||
className="divide-y divide-gray-600 rounded-md border border-gray-600 bg-gray-800/50 md:flex md:divide-y-0"
|
||||
style={{ backdropFilter: 'blur(5px)' }}
|
||||
>
|
||||
<SetupSteps
|
||||
@@ -193,7 +193,7 @@ const Setup = () => {
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800 bg-opacity-50 p-4 text-white">
|
||||
<div className="mt-10 w-full rounded-md border border-gray-600 bg-gray-800/50 p-4 text-white">
|
||||
{currentStep === 1 && (
|
||||
<div className="flex flex-col items-center pb-6">
|
||||
<div className="mb-2 flex justify-center text-xl font-bold">
|
||||
|
||||
@@ -137,12 +137,12 @@ const StatusBadge = ({
|
||||
|
||||
const badgeDownloadProgress = (
|
||||
<div
|
||||
className={`absolute left-0 top-0 z-10 flex h-full bg-opacity-80 ${
|
||||
className={`absolute left-0 top-0 z-10 flex h-full ${
|
||||
status === MediaStatus.DELETED
|
||||
? 'bg-red-600'
|
||||
? 'bg-red-600/80'
|
||||
: status === MediaStatus.PROCESSING
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-green-500'
|
||||
? 'bg-indigo-500/80'
|
||||
: 'bg-green-500/80'
|
||||
} transition-all duration-200 ease-in-out`}
|
||||
style={{
|
||||
width: `${
|
||||
@@ -168,8 +168,7 @@ const StatusBadge = ({
|
||||
badgeType="success"
|
||||
href={mediaLink}
|
||||
className={`${
|
||||
inProgress &&
|
||||
'relative !bg-gray-700 !bg-opacity-80 !px-0 hover:!bg-gray-700'
|
||||
inProgress && 'relative !bg-gray-700/80 !px-0 hover:!bg-gray-700'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{inProgress && badgeDownloadProgress}
|
||||
@@ -234,8 +233,7 @@ const StatusBadge = ({
|
||||
badgeType="success"
|
||||
href={mediaLink}
|
||||
className={`${
|
||||
inProgress &&
|
||||
'relative !bg-gray-700 !bg-opacity-80 !px-0 hover:!bg-gray-700'
|
||||
inProgress && 'relative !bg-gray-700/80 !px-0 hover:!bg-gray-700'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{inProgress && badgeDownloadProgress}
|
||||
@@ -300,8 +298,7 @@ const StatusBadge = ({
|
||||
badgeType="primary"
|
||||
href={mediaLink}
|
||||
className={`${
|
||||
inProgress &&
|
||||
'relative !bg-gray-700 !bg-opacity-80 !px-0 hover:!bg-gray-700'
|
||||
inProgress && 'relative !bg-gray-700/80 !px-0 hover:!bg-gray-700'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{inProgress && badgeDownloadProgress}
|
||||
@@ -388,8 +385,7 @@ const StatusBadge = ({
|
||||
badgeType="danger"
|
||||
href={mediaLink}
|
||||
className={`${
|
||||
inProgress &&
|
||||
'relative !bg-gray-700 !bg-opacity-80 !px-0 hover:!bg-gray-700'
|
||||
inProgress && 'relative !bg-gray-700/80 !px-0 hover:!bg-gray-700'
|
||||
} overflow-hidden`}
|
||||
>
|
||||
{inProgress && badgeDownloadProgress}
|
||||
|
||||
@@ -341,10 +341,10 @@ const TitleCard = ({
|
||||
/>
|
||||
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
||||
<div
|
||||
className={`pointer-events-none z-40 self-start rounded-full border bg-opacity-80 shadow-md ${
|
||||
className={`pointer-events-none z-40 self-start rounded-full border shadow-md ${
|
||||
mediaType === 'movie' || mediaType === 'collection'
|
||||
? 'border-blue-500 bg-blue-600'
|
||||
: 'border-purple-600 bg-purple-600'
|
||||
? 'border-blue-500 bg-blue-600/80'
|
||||
: 'border-purple-600 bg-purple-600/80'
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
||||
@@ -432,7 +432,7 @@ const TitleCard = ({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center rounded-xl bg-gray-800 bg-opacity-75 text-white">
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center rounded-xl bg-gray-800/75 text-white">
|
||||
<Spinner className="h-10 w-10" />
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -186,13 +186,13 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
className={`${
|
||||
isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
<th className="bg-gray-500 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
@@ -230,7 +230,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
@@ -238,7 +238,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
|
||||
@@ -152,13 +152,13 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
||||
className={`${
|
||||
isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
<th className="bg-gray-500 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
@@ -189,7 +189,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
@@ -197,7 +197,7 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
|
||||
@@ -53,7 +53,7 @@ const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
|
||||
<span
|
||||
className="absolute inset-0 rounded-full shadow-inner"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-1.5">
|
||||
|
||||
@@ -100,14 +100,16 @@ const UserLinkedAccountsSettings = () => {
|
||||
);
|
||||
await revalidateUser();
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 401) {
|
||||
setError(intl.formatMessage(messages.plexErrorUnauthorized));
|
||||
} else if (e?.response?.status === 422) {
|
||||
setError(intl.formatMessage(messages.plexErrorExists));
|
||||
} else {
|
||||
setError(intl.formatMessage(messages.errorUnknown));
|
||||
switch (e?.response?.status) {
|
||||
case 401:
|
||||
setError(intl.formatMessage(messages.plexErrorUnauthorized));
|
||||
break;
|
||||
case 422:
|
||||
setError(intl.formatMessage(messages.plexErrorExists));
|
||||
break;
|
||||
default:
|
||||
setError(intl.formatMessage(messages.errorUnknown));
|
||||
}
|
||||
setError(intl.formatMessage(messages.errorUnknown));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,7 +212,7 @@ const UserLinkedAccountsSettings = () => {
|
||||
{accounts.map((acct, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="flex items-center gap-4 overflow-hidden rounded-lg bg-gray-800 bg-opacity-50 px-4 py-5 shadow ring-1 ring-gray-700 sm:p-6"
|
||||
className="flex items-center gap-4 overflow-hidden rounded-lg bg-gray-800/50 px-4 py-5 shadow ring-1 ring-gray-700 sm:p-6"
|
||||
>
|
||||
<div className="w-12">
|
||||
{acct.type === LinkedAccountType.Plex ? (
|
||||
|
||||
@@ -153,7 +153,7 @@ const UserProfile = () => {
|
||||
)) && (
|
||||
<div className="relative z-40">
|
||||
<dl className="mt-5 grid grid-cols-1 gap-5 lg:grid-cols-3">
|
||||
<div className="overflow-hidden rounded-lg bg-gray-800 bg-opacity-50 px-4 py-5 shadow ring-1 ring-gray-700 sm:p-6">
|
||||
<div className="overflow-hidden rounded-lg bg-gray-800/50 px-4 py-5 shadow ring-1 ring-gray-700 sm:p-6">
|
||||
<dt className="truncate text-sm font-bold text-gray-300">
|
||||
{intl.formatMessage(messages.totalrequests)}
|
||||
</dt>
|
||||
@@ -173,7 +173,7 @@ const UserProfile = () => {
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-hidden rounded-lg bg-gray-800 bg-opacity-50 px-4 py-5 shadow ring-1 ${
|
||||
className={`overflow-hidden rounded-lg bg-gray-800/50 px-4 py-5 shadow ring-1 ${
|
||||
quota.movie.restricted
|
||||
? 'bg-gradient-to-t from-red-900 to-transparent ring-red-500'
|
||||
: 'ring-gray-700'
|
||||
@@ -228,7 +228,7 @@ const UserProfile = () => {
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-hidden rounded-lg bg-gray-800 bg-opacity-50 px-4 py-5 shadow ring-1 ${
|
||||
className={`overflow-hidden rounded-lg bg-gray-800/50 px-4 py-5 shadow ring-1 ${
|
||||
quota.tv.restricted
|
||||
? 'bg-gradient-to-t from-red-900 to-transparent ring-red-500'
|
||||
: 'ring-gray-700'
|
||||
|
||||
@@ -997,6 +997,8 @@
|
||||
"components.Settings.SettingsMain.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Settings.SettingsMain.youtubeUrl": "YouTube URL",
|
||||
"components.Settings.SettingsMain.youtubeUrlTip": "Base URL for YouTube videos if a self-hosted YouTube instance is used.",
|
||||
"components.Settings.SettingsNetwork.apiRequestTimeout": "API Request Timeout",
|
||||
"components.Settings.SettingsNetwork.apiRequestTimeoutTip": "Maximum time (in seconds) to wait for responses from external services like Radarr/Sonarr. Set to 0 for no timeout.",
|
||||
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||
@@ -1026,6 +1028,7 @@
|
||||
"components.Settings.SettingsNetwork.toastSettingsSuccess": "Settings saved successfully!",
|
||||
"components.Settings.SettingsNetwork.trustProxy": "Enable Proxy Support",
|
||||
"components.Settings.SettingsNetwork.trustProxyTip": "Allow Seerr to correctly register client IP addresses behind a proxy",
|
||||
"components.Settings.SettingsNetwork.validationApiRequestTimeout": "You must provide a valid timeout value",
|
||||
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "You must provide a valid maximum TTL",
|
||||
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "You must provide a valid minimum TTL",
|
||||
"components.Settings.SettingsNetwork.validationProxyPort": "You must provide a valid port",
|
||||
|
||||
@@ -206,7 +206,7 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1, viewport-fit=cover, width=device-width"
|
||||
></meta>
|
||||
/>
|
||||
<PWAHeader
|
||||
applicationTitle={currentSettings.applicationTitle}
|
||||
/>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
|
||||
.service-error-banner {
|
||||
@apply mb-2 flex items-center gap-2 rounded-md border border-yellow-500 bg-yellow-500 bg-opacity-20 px-3 py-2 text-sm text-yellow-200;
|
||||
@apply mb-2 flex items-center gap-2 rounded-md border border-yellow-500 bg-yellow-500/20 px-3 py-2 text-sm text-yellow-200;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
@@ -346,7 +346,7 @@
|
||||
}
|
||||
|
||||
button.input-action {
|
||||
@apply relative -ml-px inline-flex items-center border border-gray-500 bg-indigo-600 bg-opacity-80 px-3 py-2 text-sm font-medium leading-5 text-white last:rounded-r-md sm:px-3.5;
|
||||
@apply relative -ml-px inline-flex items-center border border-gray-500 bg-indigo-600/80 px-3 py-2 text-sm font-medium leading-5 text-white last:rounded-r-md sm:px-3.5;
|
||||
}
|
||||
|
||||
button.input-action[disabled] {
|
||||
@@ -354,7 +354,7 @@
|
||||
}
|
||||
|
||||
button.input-action:not([disabled]) {
|
||||
@apply transition duration-150 ease-in-out hover:bg-opacity-100 active:bg-gray-100 active:text-gray-700;
|
||||
@apply transition duration-150 ease-in-out hover:bg-indigo-600 active:bg-gray-100 active:text-gray-700;
|
||||
}
|
||||
|
||||
.button-md :where(svg),
|
||||
|
||||
Reference in New Issue
Block a user