diff --git a/.eslintrc.js b/.eslintrc.js index ec5570706..053f62529 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,8 +45,10 @@ module.exports = { overrides: [ { files: ['**/*.tsx'], + plugins: ['react'], rules: { 'react/prop-types': 'off', + 'react/self-closing-comp': 'error', }, }, ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7f797465..674b466b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 000000000..e532d6be1 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -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} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b79496b7d..9bf72c7ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 < [!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. diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 236358890..2b531ae12 100644 --- a/charts/seerr-chart/Chart.yaml +++ b/charts/seerr-chart/Chart.yaml @@ -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: diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index 49e965e87..9b0546ee4 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -1,6 +1,6 @@ # seerr-chart -![Version: 3.1.0](https://img.shields.io/badge/Version-3.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.0.1](https://img.shields.io/badge/AppVersion-v3.0.1-informational?style=flat-square) +![Version: 3.2.0](https://img.shields.io/badge/Version-3.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.0.1](https://img.shields.io/badge/AppVersion-v3.0.1-informational?style=flat-square) 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` | | diff --git a/charts/seerr-chart/templates/route.yaml b/charts/seerr-chart/templates/route.yaml new file mode 100644 index 000000000..a7fd0ed67 --- /dev/null +++ b/charts/seerr-chart/templates/route.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/seerr-chart/values.yaml b/charts/seerr-chart/values.yaml index 6da7cf453..40378b6ac 100644 --- a/charts/seerr-chart/values.yaml +++ b/charts/seerr-chart/values.yaml @@ -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 diff --git a/docs/extending-seerr/database-config.mdx b/docs/extending-seerr/database-config.mdx index e2003b19b..59684161c 100644 --- a/docs/extending-seerr/database-config.mdx +++ b/docs/extending-seerr/database-config.mdx @@ -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. diff --git a/docs/getting-started/third-parties/synology.mdx b/docs/getting-started/third-parties/synology.mdx new file mode 100644 index 000000000..6a5e6d2af --- /dev/null +++ b/docs/getting-started/third-parties/synology.mdx @@ -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://: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. +::: diff --git a/docs/getting-started/third-parties/truenas.mdx b/docs/getting-started/third-parties/truenas.mdx index b0e261ccc..aa03e7266 100644 --- a/docs/getting-started/third-parties/truenas.mdx +++ b/docs/getting-started/third-parties/truenas.mdx @@ -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. -::: - - + +## 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. diff --git a/docs/getting-started/third-parties/unraid.mdx b/docs/getting-started/third-parties/unraid.mdx index bb53b9b78..e07dcf367 100644 --- a/docs/getting-started/third-parties/unraid.mdx +++ b/docs/getting-started/third-parties/unraid.mdx @@ -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 -``` + + -### 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://: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://:5055` and follow the setup wizard. + + + + + ### 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://:5055` and follow the setup wizard. + + + diff --git a/docs/migration-guide.mdx b/docs/migration-guide.mdx index 2bc039eb7..e99d95a2a 100644 --- a/docs/migration-guide.mdx +++ b/docs/migration-guide.mdx @@ -200,6 +200,11 @@ Summary of changes : ## 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. + + + + **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 + + + **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/ + ``` + 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 + + ### 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`. -::: \ No newline at end of file +**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. \ No newline at end of file diff --git a/gen-docs/blog/2026-02-10/seerr-release.md b/gen-docs/blog/2026-02-10/seerr-release.md index fda587b33..a3d02a794 100644 --- a/gen-docs/blog/2026-02-10/seerr-release.md +++ b/gen-docs/blog/2026-02-10/seerr-release.md @@ -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. diff --git a/gen-docs/docusaurus.config.ts b/gen-docs/docusaurus.config.ts index b50624119..94b7d8828 100644 --- a/gen-docs/docusaurus.config.ts +++ b/gen-docs/docusaurus.config.ts @@ -16,7 +16,12 @@ const config: Config = { deploymentBranch: 'gh-pages', onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', + + markdown: { + hooks: { + onBrokenMarkdownLinks: 'warn', + }, + }, i18n: { defaultLocale: 'en', diff --git a/package.json b/package.json index d38b779d8..d68daad5d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index cdbb2203e..5e18f5b49 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -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 extends ExternalAPI { apiKey, cacheName, apiName, - timeout = 10000, }: { url: string; apiKey: string; cacheName: AvailableCacheIds; apiName: string; - timeout?: number; }) { + const timeout = getSettings().network.apiRequestTimeout; + super( url, { diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 29f492d73..75f320269 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -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 => { @@ -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; } diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index e8b39c6f7..01b429ba5 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -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 { diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 25609c58b..8737ad0e0 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -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; diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index bb3456c7e..f44159af4 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -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, + }); } } diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index 24862e558..b464008e8 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -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); diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 7a6e95c0a..3c39519d2 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -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( diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 0aa9da159..1e659cd29 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -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: [], }; diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index be73a0dfd..8afea0df5 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -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)) ) ); diff --git a/server/routes/media.ts b/server/routes/media.ts index 8f52efae8..d7962825d 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -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 = diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 683539680..12b574659 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -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( diff --git a/src/components/AppDataWarning/index.tsx b/src/components/AppDataWarning/index.tsx index 40874d5d2..052a78d0e 100644 --- a/src/components/AppDataWarning/index.tsx +++ b/src/components/AppDataWarning/index.tsx @@ -28,7 +28,7 @@ const AppDataWarning = () => { ( - {msg} + {msg} ), appDataPath: data.appDataPath, })} diff --git a/src/components/Blocklist/index.tsx b/src/components/Blocklist/index.tsx index 63c95a245..daacf3397 100644 --- a/src/components/Blocklist/index.tsx +++ b/src/components/Blocklist/index.tsx @@ -441,13 +441,13 @@ const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => { )}
{item.mediaType === 'movie' ? ( -
+
{intl.formatMessage(globalMessages.movie)}
) : ( -
+
{intl.formatMessage(globalMessages.tvshow)}
diff --git a/src/components/Common/Alert/index.tsx b/src/components/Common/Alert/index.tsx index 93ddd988f..783b1d7d3 100644 --- a/src/components/Common/Alert/index.tsx +++ b/src/components/Common/Alert/index.tsx @@ -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: , @@ -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: , diff --git a/src/components/Common/Badge/index.tsx b/src/components/Common/Badge/index.tsx index 0b003d3a2..5ee23bdfd 100644 --- a/src/components/Common/Badge/index.tsx +++ b/src/components/Common/Badge/index.tsx @@ -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'); } } diff --git a/src/components/Common/Button/index.tsx b/src/components/Common/Button/index.tsx index e29c78d47..eb422f224 100644 --- a/src/components/Common/Button/index.tsx +++ b/src/components/Common/Button/index.tsx @@ -52,22 +52,22 @@ function 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

( 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' ); } diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index 36c793646..21a88d206 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -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'; diff --git a/src/components/Common/Dropdown/index.tsx b/src/components/Common/Dropdown/index.tsx index b8d8f7fd9..9a8501b06 100644 --- a/src/components/Common/Dropdown/index.tsx +++ b/src/components/Common/Dropdown/index.tsx @@ -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} diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx index 055e66c8b..12f679112 100644 --- a/src/components/Common/Modal/index.tsx +++ b/src/components/Common/Modal/index.tsx @@ -91,7 +91,7 @@ const Modal = React.forwardRef( { className={`${ checked ? 'bg-indigo-500' : 'bg-gray-700' } absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`} - > + /> + /> ); }; diff --git a/src/components/Common/SlideOver/index.tsx b/src/components/Common/SlideOver/index.tsx index bbe8985a3..7b06ae730 100644 --- a/src/components/Common/SlideOver/index.tsx +++ b/src/components/Common/SlideOver/index.tsx @@ -46,7 +46,7 @@ const SlideOver = ({ > {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}

onClose()} onKeyDown={(e) => { if (e.key === 'Escape') { @@ -71,7 +71,7 @@ const SlideOver = ({ ref={slideoverRef} onClick={(e) => e.stopPropagation()} > -
+

diff --git a/src/components/Common/StatusBadgeMini/index.tsx b/src/components/Common/StatusBadgeMini/index.tsx index b0d6f8def..ea93a2a9b 100644 --- a/src/components/Common/StatusBadgeMini/index.tsx +++ b/src/components/Common/StatusBadgeMini/index.tsx @@ -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 = ; 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 = ; 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 = ; 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 = ; 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 = ; 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 = ; break; } diff --git a/src/components/Discover/CreateSlider/index.tsx b/src/components/Discover/CreateSlider/index.tsx index 9c7493d2a..e7162de22 100644 --- a/src/components/Discover/CreateSlider/index.tsx +++ b/src/components/Discover/CreateSlider/index.tsx @@ -508,7 +508,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => { typeof errors.data === 'string' && (
{errors.data}
)} -
+
{resultCount === 0 ? (
diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 3ce6a76e9..97583d755 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -159,7 +159,7 @@ const Discover = () => { @@ -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" >