Compare commits

..

2 Commits

Author SHA1 Message Date
fallenbagel
1955db5c35 docs: add documentation for unix socket postgres connection 2025-01-12 12:13:07 +08:00
fallenbagel
ee803b601c refactor: adds socket_path for unix socket support for postgres 2025-01-12 12:12:23 +08:00
184 changed files with 5068 additions and 9810 deletions

View File

@@ -7,7 +7,7 @@
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
"contributorsPerLine": 7,
"projectName": "jellyseerr",
"projectOwner": "fallenbagel",
"projectOwner": "Fallenbagel",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,
@@ -94,8 +94,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/345752?v=4",
"profile": "https://github.com/jab416171",
"contributions": [
"doc",
"code"
"doc"
]
},
{
@@ -339,8 +338,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
"profile": "https://gauthierth.fr/",
"contributions": [
"code",
"maintenance"
"code"
]
},
{
@@ -558,159 +556,6 @@
"contributions": [
"code"
]
},
{
"login": "GkhnGRBZ",
"name": "GkhnGRBZ",
"avatar_url": "https://avatars.githubusercontent.com/u/127258824?v=4",
"profile": "https://github.com/GkhnGRBZ",
"contributions": [
"code"
]
},
{
"login": "benhaney",
"name": "Ben Haney",
"avatar_url": "https://avatars.githubusercontent.com/u/31331498?v=4",
"profile": "http://benhaney.com",
"contributions": [
"code"
]
},
{
"login": "Wunderharke",
"name": "Wunderharke",
"avatar_url": "https://avatars.githubusercontent.com/u/5105672?v=4",
"profile": "https://github.com/Wunderharke",
"contributions": [
"doc"
]
},
{
"login": "methbkts",
"name": "Metin Bektas",
"avatar_url": "https://avatars.githubusercontent.com/u/30674934?v=4",
"profile": "https://github.com/methbkts",
"contributions": [
"infra"
]
},
{
"login": "andrewkolda",
"name": "andrewkolda",
"avatar_url": "https://avatars.githubusercontent.com/u/158614532?v=4",
"profile": "https://github.com/andrewkolda",
"contributions": [
"design"
]
},
{
"login": "ishanjain28",
"name": "Ishan Jain",
"avatar_url": "https://avatars.githubusercontent.com/u/7921368?v=4",
"profile": "https://ishanjain.me",
"contributions": [
"code"
]
},
{
"login": "michaelhthomas",
"name": "Michael Thomas",
"avatar_url": "https://avatars.githubusercontent.com/u/18223295?v=4",
"profile": "http://michaelt.xyz",
"contributions": [
"code"
]
},
{
"login": "j0srisk",
"name": "Joseph Risk",
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
"profile": "http://josephrisk.com",
"contributions": [
"code"
]
},
{
"login": "Loetwiek",
"name": "Loetwiek",
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
"profile": "https://github.com/Loetwiek",
"contributions": [
"code"
]
},
{
"login": "Fuochi",
"name": "Fuochi",
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
"profile": "https://github.com/Fuochi",
"contributions": [
"doc"
]
},
{
"login": "demrich",
"name": "David Emrich",
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
"profile": "https://github.com/demrich",
"contributions": [
"code"
]
},
{
"login": "maxnatamo",
"name": "Max T. Kristiansen",
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
"profile": "https://maxtrier.dk",
"contributions": [
"code"
]
},
{
"login": "DamsDev1",
"name": "Damien Fajole",
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
"profile": "https://damsdev.me",
"contributions": [
"code"
]
},
{
"login": "AhmedNSidd",
"name": "Ahmed Siddiqui",
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
"profile": "https://github.com/AhmedNSidd",
"contributions": [
"code"
]
},
{
"login": "JackW6809",
"name": "JackOXI",
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
"profile": "https://github.com/JackW6809",
"contributions": [
"code"
]
},
{
"login": "StancuFlorin",
"name": "Stancu Florin",
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
"profile": "http://indicus.ro",
"contributions": [
"code"
]
},
{
"login": "RankWeis",
"name": "RankWeis",
"avatar_url": "https://avatars.githubusercontent.com/u/733691?v=4",
"profile": "https://github.com/RankWeis",
"contributions": [
"code"
]
}
]
}

View File

@@ -63,8 +63,6 @@ body:
- PostgreSQL
label: Database
description: Which database backend are you using?
validations:
required: true
- type: input
id: device
attributes:

View File

@@ -12,7 +12,7 @@ jobs:
test:
name: Lint & Test Build
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
container: node:22-alpine
steps:
- name: Checkout
@@ -43,23 +43,15 @@ jobs:
- name: Build
run: pnpm build
build:
build_and_push:
name: Build & Publish Docker Images
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
strategy:
matrix:
include:
- runner: ubuntu-24.04
platform: linux/amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
runs-on: ${{ matrix.runner }}
outputs:
digest-amd64: ${{ steps.set_outputs.outputs.digest-amd64 }}
digest-arm64: ${{ steps.set_outputs.outputs.digest-arm64 }}
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
@@ -78,77 +70,24 @@ jobs:
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: ${{ github.repository_owner }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
fallenbagel/jellyseerr
ghcr.io/${{ env.OWNER_LC }}/jellyseerr
tags: |
type=ref,event=branch
type=sha,prefix=,suffix=,format=short
- name: Build and push by digest
id: build
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platform }}
platforms: linux/amd64,linux/arm64
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}
outputs: |
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
provenance: false
- name: Set outputs
id: set_outputs
run: |
platform="${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}"
echo "digest-${platform}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
merge_and_push:
name: Create and Push Multi-arch Manifest
needs: build
runs-on: ubuntu-24.04
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: ${{ github.repository_owner }}
- name: Create and push manifest
run: |
docker manifest create fallenbagel/jellyseerr:develop \
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
docker manifest push fallenbagel/jellyseerr:develop
# GHCR manifest
docker manifest create ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop \
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
docker manifest push ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
tags: |
fallenbagel/jellyseerr:develop
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
discord:
name: Send Discord Notification
needs: merge_and_push
needs: build_and_push
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v3

View File

@@ -36,10 +36,3 @@ jobs:
# Fix test titles in cypress dashboard
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
- name: Upload video files
uses: actions/upload-artifact@v4
with:
name: cypress-videos
path: |
cypress/videos
cypress/screenshots

View File

@@ -1,135 +0,0 @@
name: Release Charts
on:
push:
branches:
- develop
jobs:
package-helm-chart:
name: Package helm chart
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install helm
uses: azure/setup-helm@v4
- name: Install Oras
uses: oras-project/setup-oras@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Package helm charts
run: |
mkdir -p ./.cr-release-packages
for chart_path in ./charts/*; do
if [ -d "$chart_path" ] && [ -f "$chart_path/Chart.yaml" ]; then
chart_name=$(grep '^name:' "$chart_path/Chart.yaml" | awk '{print $2}')
# get current version
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
# try to get current release version
set +e
oras discover ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}
oras_exit_code=$?
set -e
if [ $oras_exit_code -ne 0 ]; then
helm dependency build "$chart_path"
helm package "$chart_path" --destination ./.cr-release-packages
else
echo "No version change for $chart_name. Skipping."
fi
else
echo "Skipping $chart_name: Not a valid Helm chart"
fi
done
- name: Check if artifacts exist
id: check-artifacts
run: |
if ls .cr-release-packages/* >/dev/null 2>&1; then
echo "has_artifacts=true" >> $GITHUB_OUTPUT
else
echo "has_artifacts=false" >> $GITHUB_OUTPUT
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: steps.check-artifacts.outputs.has_artifacts == 'true'
with:
name: artifacts
include-hidden-files: true
path: .cr-release-packages/
publish:
name: Publish to ghcr.io
runs-on: ubuntu-latest
permissions:
packages: write # needed for pushing to github registry
id-token: write # needed for signing the images with GitHub OIDC Token
needs: [package-helm-chart]
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install helm
uses: azure/setup-helm@v4
- name: Install Oras
uses: oras-project/setup-oras@v1
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Downloads artifacts
uses: actions/download-artifact@v4
with:
name: artifacts
path: .cr-release-packages/
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push charts to GHCR
env:
COSIGN_YES: true
run: |
for chart_path in `find .cr-release-packages -name '*.tgz' -print`; do
# push chart to OCI
chart_release_file=$(basename "$chart_path")
chart_name=${chart_release_file%-*}
helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log
chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log)
# sign chart
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}"
# push artifacthub-repo.yml to OCI
oras push \
ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \
|& tee oras-push-output.log
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
# sign artifacthub-repo.yml
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}"
done

View File

@@ -26,12 +26,6 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_TOKEN }}
- name: Pnpm Setup
uses: pnpm/action-setup@v4
with:

View File

@@ -97,7 +97,7 @@ When adding new UI text, please try to adhere to the following guidelines:
## Translation
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Jellyseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>

View File

@@ -14,7 +14,7 @@ RUN \
;; \
esac
RUN npm install --global pnpm@9
RUN npm install --global pnpm
COPY package.json pnpm-lock.yaml postinstall-win.js ./
RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
@@ -29,7 +29,7 @@ RUN pnpm build
# remove development dependencies
RUN pnpm prune --prod --ignore-scripts
RUN rm -rf src server .next/cache charts gen-docs docs
RUN rm -rf src server .next/cache
RUN touch config/DOCKER
@@ -45,7 +45,7 @@ WORKDIR /app
RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
RUN npm install -g pnpm@9
RUN npm install -g pnpm
# copy from build image
COPY --from=BUILD_IMAGE /app ./

View File

@@ -3,7 +3,7 @@ FROM node:22-alpine
COPY . /app
WORKDIR /app
RUN npm install --global pnpm@9
Run npm install --global pnpm
RUN pnpm install

132
README.md
View File

@@ -11,17 +11,17 @@
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-77-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-60-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
**Jellyseerr** is a free and open source software application for managing requests for your media library.
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring additional support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
## Current Features
- Full Jellyfin/Emby/Plex integration including authentication with user import & management.
- Support for **PostgreSQL** and **SQLite** databases.
- Supports Movies, Shows and Mixed Libraries.
- Ability to change email addresses for SMTP purposes.
- Full Jellyfin/Emby/Plex integration including authentication with user import & management
- Supports Movies, Shows and Mixed Libraries
- Ability to change email addresses for smtp purposes
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
@@ -29,7 +29,8 @@
- Granular permission system.
- Support for various notification agents.
- Mobile-friendly design, for when you need to approve requests on the go!
- Support for watchlisting & blacklisting media.
(Upcoming Features include: Multiple Server Instances, and much more!)
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
@@ -86,103 +87,82 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Code">💻</a> <a href="#maintenance-Fallenbagel" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanzhang98"><img src="https://avatars.githubusercontent.com/u/34902361?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="#translation-seanzhang98" title="Translation">🌍</a> <a href="https://github.com/fallenbagel/jellyseerr/commits?author=seanzhang98" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notfakie"><img src="https://avatars.githubusercontent.com/u/103784113?v=4?s=100" width="100px;" alt="notfakie"/><br /><sub><b>notfakie</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=notfakie" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jumail"><img src="https://avatars.githubusercontent.com/u/7672055?v=4?s=100" width="100px;" alt="Mohamed Jumail"/><br /><sub><b>Mohamed Jumail</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/pulls?q=is%3Apr+reviewed-by%3AJumail" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.heywhale.com"><img src="https://avatars.githubusercontent.com/u/4048787?v=4?s=100" width="100px;" alt="Shilong Jiang"/><br /><sub><b>Shilong Jiang</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jsl9208" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jinas.me"><img src="https://avatars.githubusercontent.com/u/28459081?v=4?s=100" width="100px;" alt="Boring Dragon"/><br /><sub><b>Boring Dragon</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=boring-dragon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=sambartik" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Code">💻</a> <a href="#maintenance-Fallenbagel" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanzhang98"><img src="https://avatars.githubusercontent.com/u/34902361?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="#translation-seanzhang98" title="Translation">🌍</a> <a href="https://github.com/Fallenbagel/jellyseerr/commits?author=seanzhang98" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notfakie"><img src="https://avatars.githubusercontent.com/u/103784113?v=4?s=100" width="100px;" alt="notfakie"/><br /><sub><b>notfakie</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=notfakie" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jumail"><img src="https://avatars.githubusercontent.com/u/7672055?v=4?s=100" width="100px;" alt="Mohamed Jumail"/><br /><sub><b>Mohamed Jumail</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/pulls?q=is%3Apr+reviewed-by%3AJumail" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.heywhale.com"><img src="https://avatars.githubusercontent.com/u/4048787?v=4?s=100" width="100px;" alt="Shilong Jiang"/><br /><sub><b>Shilong Jiang</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jsl9208" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jinas.me"><img src="https://avatars.githubusercontent.com/u/28459081?v=4?s=100" width="100px;" alt="Boring Dragon"/><br /><sub><b>Boring Dragon</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=boring-dragon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=sambartik" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a> <a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Smexhy"><img src="https://avatars.githubusercontent.com/u/4880625?v=4?s=100" width="100px;" alt="Smexhy"/><br /><sub><b>Smexhy</b></sub></a><br /><a href="#translation-Smexhy" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://qwer.tz"><img src="https://avatars.githubusercontent.com/u/71837281?v=4?s=100" width="100px;" alt="Daniel"/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=darmiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://qwer.tz"><img src="https://avatars.githubusercontent.com/u/71837281?v=4?s=100" width="100px;" alt="Daniel"/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=darmiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/undone37"><img src="https://avatars.githubusercontent.com/u/10513808?v=4?s=100" width="100px;" alt="undone37"/><br /><sub><b>undone37</b></sub></a><br /><a href="#translation-undone37" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CheChu10"><img src="https://avatars.githubusercontent.com/u/32913133?v=4?s=100" width="100px;" alt="Chechu García"/><br /><sub><b>Chechu García</b></sub></a><br /><a href="#translation-CheChu10" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DimitriDR"><img src="https://avatars.githubusercontent.com/u/56969769?v=4?s=100" width="100px;" alt="Dimitri"/><br /><sub><b>Dimitri</b></sub></a><br /><a href="#translation-DimitriDR" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrey4korop"><img src="https://avatars.githubusercontent.com/u/24610708?v=4?s=100" width="100px;" alt="andrey4korop"/><br /><sub><b>andrey4korop</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=andrey4korop" title="Code">💻</a> <a href="#translation-andrey4korop" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrey4korop"><img src="https://avatars.githubusercontent.com/u/24610708?v=4?s=100" width="100px;" alt="andrey4korop"/><br /><sub><b>andrey4korop</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=andrey4korop" title="Code">💻</a> <a href="#translation-andrey4korop" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://geoffrey-coulaud.fr"><img src="https://avatars.githubusercontent.com/u/20744730?v=4?s=100" width="100px;" alt="Geoffrey Coulaud"/><br /><sub><b>Geoffrey Coulaud</b></sub></a><br /><a href="#translation-GeoffreyCoulaud" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pikachu920"><img src="https://avatars.githubusercontent.com/u/28607612?v=4?s=100" width="100px;" alt="Pikachu920"/><br /><sub><b>Pikachu920</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Pikachu920" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yalagin"><img src="https://avatars.githubusercontent.com/u/12879142?v=4?s=100" width="100px;" alt="Maxim Yalagin"/><br /><sub><b>Maxim Yalagin</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=yalagin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeaboswell"><img src="https://avatars.githubusercontent.com/u/11653068?v=4?s=100" width="100px;" alt="Jesse Boswell"/><br /><sub><b>Jesse Boswell</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jeaboswell" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pikachu920"><img src="https://avatars.githubusercontent.com/u/28607612?v=4?s=100" width="100px;" alt="Pikachu920"/><br /><sub><b>Pikachu920</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Pikachu920" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yalagin"><img src="https://avatars.githubusercontent.com/u/12879142?v=4?s=100" width="100px;" alt="Maxim Yalagin"/><br /><sub><b>Maxim Yalagin</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=yalagin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeaboswell"><img src="https://avatars.githubusercontent.com/u/11653068?v=4?s=100" width="100px;" alt="Jesse Boswell"/><br /><sub><b>Jesse Boswell</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jeaboswell" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/d-fendrich"><img src="https://avatars.githubusercontent.com/u/27904138?v=4?s=100" width="100px;" alt="d-fendrich"/><br /><sub><b>d-fendrich</b></sub></a><br /><a href="#translation-d-fendrich" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidfdezalcoba"><img src="https://avatars.githubusercontent.com/u/15996018?v=4?s=100" width="100px;" alt="David Fernández Alcoba"/><br /><sub><b>David Fernández Alcoba</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=davidfdezalcoba" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidfdezalcoba"><img src="https://avatars.githubusercontent.com/u/15996018?v=4?s=100" width="100px;" alt="David Fernández Alcoba"/><br /><sub><b>David Fernández Alcoba</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=davidfdezalcoba" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gauvino"><img src="https://avatars.githubusercontent.com/u/68083474?v=4?s=100" width="100px;" alt="Gauvino"/><br /><sub><b>Gauvino</b></sub></a><br /><a href="#translation-Gauvino" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EthanArmbrust"><img src="https://avatars.githubusercontent.com/u/22754714?v=4?s=100" width="100px;" alt="EthanArmbrust"/><br /><sub><b>EthanArmbrust</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=EthanArmbrust" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.piribisoft.com"><img src="https://avatars.githubusercontent.com/u/854646?v=4?s=100" width="100px;" alt="Eduardo"/><br /><sub><b>Eduardo</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=SirMartin" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RickLuiken"><img src="https://avatars.githubusercontent.com/u/34110371?v=4?s=100" width="100px;" alt="RickLuiken"/><br /><sub><b>RickLuiken</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=RickLuiken" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EthanArmbrust"><img src="https://avatars.githubusercontent.com/u/22754714?v=4?s=100" width="100px;" alt="EthanArmbrust"/><br /><sub><b>EthanArmbrust</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=EthanArmbrust" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.piribisoft.com"><img src="https://avatars.githubusercontent.com/u/854646?v=4?s=100" width="100px;" alt="Eduardo"/><br /><sub><b>Eduardo</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=SirMartin" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RickLuiken"><img src="https://avatars.githubusercontent.com/u/34110371?v=4?s=100" width="100px;" alt="RickLuiken"/><br /><sub><b>RickLuiken</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=RickLuiken" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Br33ce"><img src="https://avatars.githubusercontent.com/u/124933490?v=4?s=100" width="100px;" alt="Br33ce"/><br /><sub><b>Br33ce</b></sub></a><br /><a href="#translation-Br33ce" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a> <a href="#maintenance-gauthier-th" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mobihen.com"><img src="https://avatars.githubusercontent.com/u/35529491?v=4?s=100" width="100px;" alt="Nir Israel Hen"/><br /><sub><b>Nir Israel Hen</b></sub></a><br /><a href="#translation-mobihen" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M0NsTeRRR"><img src="https://avatars.githubusercontent.com/u/37785089?v=4?s=100" width="100px;" alt="Ludovic Ortega"/><br /><sub><b>Ludovic Ortega</b></sub></a><br /><a href="#security-M0NsTeRRR" title="Security">🛡️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zariel"><img src="https://avatars.githubusercontent.com/u/2213?v=4?s=100" width="100px;" alt="Chris Bannister"/><br /><sub><b>Chris Bannister</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Zariel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zariel"><img src="https://avatars.githubusercontent.com/u/2213?v=4?s=100" width="100px;" alt="Chris Bannister"/><br /><sub><b>Chris Bannister</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Zariel" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/C4J3"><img src="https://avatars.githubusercontent.com/u/13005453?v=4?s=100" width="100px;" alt="Joe"/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=C4J3" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.garnx.fr"><img src="https://avatars.githubusercontent.com/u/37373941?v=4?s=100" width="100px;" alt="Guillaume ARNOUX"/><br /><sub><b>Guillaume ARNOUX</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=guillaumearnx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dr-carrot"><img src="https://avatars.githubusercontent.com/u/17272571?v=4?s=100" width="100px;" alt="dr-carrot"/><br /><sub><b>dr-carrot</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dr-carrot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gageorsburn"><img src="https://avatars.githubusercontent.com/u/4692734?v=4?s=100" width="100px;" alt="Gage Orsburn"/><br /><sub><b>Gage Orsburn</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gageorsburn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GkhnGRBZ"><img src="https://avatars.githubusercontent.com/u/127258824?v=4?s=100" width="100px;" alt="GkhnGRBZ"/><br /><sub><b>GkhnGRBZ</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=GkhnGRBZ" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://benhaney.com"><img src="https://avatars.githubusercontent.com/u/31331498?v=4?s=100" width="100px;" alt="Ben Haney"/><br /><sub><b>Ben Haney</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=benhaney" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wunderharke"><img src="https://avatars.githubusercontent.com/u/5105672?v=4?s=100" width="100px;" alt="Wunderharke"/><br /><sub><b>Wunderharke</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Wunderharke" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/methbkts"><img src="https://avatars.githubusercontent.com/u/30674934?v=4?s=100" width="100px;" alt="Metin Bektas"/><br /><sub><b>Metin Bektas</b></sub></a><br /><a href="#infra-methbkts" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrewkolda"><img src="https://avatars.githubusercontent.com/u/158614532?v=4?s=100" width="100px;" alt="andrewkolda"/><br /><sub><b>andrewkolda</b></sub></a><br /><a href="#design-andrewkolda" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ishanjain.me"><img src="https://avatars.githubusercontent.com/u/7921368?v=4?s=100" width="100px;" alt="Ishan Jain"/><br /><sub><b>Ishan Jain</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ishanjain28" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://michaelt.xyz"><img src="https://avatars.githubusercontent.com/u/18223295?v=4?s=100" width="100px;" alt="Michael Thomas"/><br /><sub><b>Michael Thomas</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=michaelhthomas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JackW6809" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RankWeis"><img src="https://avatars.githubusercontent.com/u/733691?v=4?s=100" width="100px;" alt="RankWeis"/><br /><sub><b>RankWeis</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=RankWeis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/C4J3"><img src="https://avatars.githubusercontent.com/u/13005453?v=4?s=100" width="100px;" alt="Joe"/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=C4J3" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.garnx.fr"><img src="https://avatars.githubusercontent.com/u/37373941?v=4?s=100" width="100px;" alt="Guillaume ARNOUX"/><br /><sub><b>Guillaume ARNOUX</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=guillaumearnx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dr-carrot"><img src="https://avatars.githubusercontent.com/u/17272571?v=4?s=100" width="100px;" alt="dr-carrot"/><br /><sub><b>dr-carrot</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=dr-carrot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gageorsburn"><img src="https://avatars.githubusercontent.com/u/4692734?v=4?s=100" width="100px;" alt="Gage Orsburn"/><br /><sub><b>Gage Orsburn</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gageorsburn" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -300,7 +280,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://github.com/byakurau"><img src="https://avatars.githubusercontent.com/u/1811683?v=4?s=100" width="100px;" alt="byakurau"/><br /><sub><b>byakurau</b></sub></a><br /><a href="#translation-byakurau" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miknii"><img src="https://avatars.githubusercontent.com/u/109232569?v=4?s=100" width="100px;" alt="miknii"/><br /><sub><b>miknii</b></sub></a><br /><a href="#translation-miknii" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eclipseop"><img src="https://avatars.githubusercontent.com/u/5846213?v=4?s=100" width="100px;" alt="Mackenzie"/><br /><sub><b>Mackenzie</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Eclipseop" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0up4200"><img src="https://avatars.githubusercontent.com/u/18177310?v=4?s=100" width="100px;" alt="soup"/><br /><sub><b>soup</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0up4200"><img src="https://avatars.githubusercontent.com/u/18177310?v=4?s=100" width="100px;" alt="soup"/><br /><sub><b>soup</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceptonit"><img src="https://avatars.githubusercontent.com/u/12678743?v=4?s=100" width="100px;" alt="ceptonit"/><br /><sub><b>ceptonit</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ceptonit" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aedelbro"><img src="https://avatars.githubusercontent.com/u/36162221?v=4?s=100" width="100px;" alt="aedelbro"/><br /><sub><b>aedelbro</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=aedelbro" title="Code">💻</a></td>
</tr>
@@ -333,8 +313,6 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JackW6809" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -1 +0,0 @@
repositoryID: c6b3f2dc-444c-4e37-b397-6a5ff563ee8b

View File

@@ -21,5 +21,3 @@
.idea/
*.tmproj
.vscode/
# go template
*.gotmpl

View File

@@ -1,10 +1,10 @@
apiVersion: v2
kubeVersion: ">=1.23.0-0"
name: jellyseerr-chart
name: Jellyseerr
description: Jellyseerr helm chart for Kubernetes
type: application
version: 2.1.1
appVersion: "2.3.0"
version: 1.1.0
appVersion: "2.1.0"
maintainers:
- name: Jellyseerr
url: https://github.com/Fallenbagel/jellyseerr

View File

@@ -1,6 +1,6 @@
# jellyseerr-chart
# Jellyseerr
![Version: 2.1.1](https://img.shields.io/badge/Version-2.1.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.3.0](https://img.shields.io/badge/AppVersion-2.3.0-informational?style=flat-square)
![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.1.0](https://img.shields.io/badge/AppVersion-2.1.0-informational?style=flat-square)
Jellyseerr helm chart for Kubernetes
@@ -25,6 +25,10 @@ Kubernetes: `>=1.23.0-0`
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| autoscaling.enabled | bool | `false` | |
| autoscaling.maxReplicas | int | `100` | |
| autoscaling.minReplicas | int | `1` | |
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
| config | object | `{"persistence":{"accessModes":["ReadWriteOnce"],"annotations":{},"name":"","size":"5Gi","volumeName":""}}` | Creating PVC to store configuration |
| config.persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes of persistent disk |
| config.persistence.annotations | object | `{}` | Annotations for PVCs |
@@ -35,7 +39,7 @@ Kubernetes: `>=1.23.0-0`
| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.registry | string | `"ghcr.io"` | |
| image.registry | string | `"docker.io"` | |
| image.repository | string | `"fallenbagel/jellyseerr"` | |
| image.sha | string | `""` | |
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
@@ -63,5 +67,3 @@ Kubernetes: `>=1.23.0-0`
| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
| strategy | object | `{"type":"Recreate"}` | Deployment strategy |
| tolerations | list | `[]` | |
| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. |
| volumes | list | `[]` | Additional volumes on the output Deployment definition. |

View File

@@ -5,7 +5,9 @@ metadata:
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: {{ .Values.strategy.type }}
selector:
@@ -65,16 +67,10 @@ spec:
volumeMounts:
- name: config
mountPath: /app/config
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
persistentVolumeClaim:
claimName: {{ include "jellyseerr.configPersistenceName" . }}
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View File

@@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "jellyseerr.fullname" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "jellyseerr.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -1,7 +1,7 @@
replicaCount: 1
image:
registry: ghcr.io
registry: docker.io
repository: fallenbagel/jellyseerr
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
@@ -94,18 +94,12 @@ resources: {}
# cpu: 100m
# memory: 128Mi
# -- Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# -- Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}

View File

@@ -4,7 +4,6 @@ export default defineConfig({
projectId: 'xkm1b4',
e2e: {
baseUrl: 'http://localhost:5055',
video: true,
experimentalSessionAndOrigin: true,
},
env: {

View File

@@ -4,7 +4,7 @@
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
"main": {
"apiKey": "testkey",
"applicationTitle": "Jellyseerr",
"applicationTitle": "Overseerr",
"applicationUrl": "",
"csrfProtection": false,
"cacheImages": false,
@@ -23,7 +23,6 @@
"mediaServerType": 1,
"partialRequestsEnabled": true,
"enableSpecialEpisodes": false,
"forceIpv4First": false,
"locale": "en"
},
"plex": {
@@ -70,7 +69,7 @@
"ignoreTls": false,
"requireTls": false,
"allowSelfSigned": false,
"senderName": "Jellyseerr"
"senderName": "Overseerr"
}
},
"discord": {

View File

@@ -13,10 +13,10 @@ describe('General Settings', () => {
});
it('modifies setting that requires restart', () => {
cy.visit('/settings/network');
cy.visit('/settings');
cy.get('#trustProxy').click();
cy.get('[data-testid=settings-network-form]').submit();
cy.get('[data-testid=settings-main-form]').submit();
cy.get('[data-testid=modal-title]').should(
'contain',
'Server Restart Required'
@@ -26,7 +26,7 @@ describe('General Settings', () => {
cy.get('[data-testid=modal-title]').should('not.exist');
cy.get('[type=checkbox]#trustProxy').click();
cy.get('[data-testid=settings-network-form]').submit();
cy.get('[data-testid=settings-main-form]').submit();
cy.get('[data-testid=modal-title]').should('not.exist');
});
});

View File

@@ -6,6 +6,7 @@ Cypress.Commands.add('login', (email, password) => {
[email, password],
() => {
cy.visit('/login');
cy.contains('Use your Overseerr account').click();
cy.get('[data-testid=email]').type(email);
cy.get('[data-testid=password]').type(password);

View File

@@ -7,34 +7,31 @@ sidebar_position: 1
Welcome to the Jellyseerr Documentation.
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
## Features
- **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex.
- **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have.
- Supports Movies, Shows and Mixed Libraries.
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
- Optionally set **Override rules** for requests to match with your defined conditions.
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
- **Simple request management UI**. Don't dig through the app to approve recent requests.
- **Mobile-friendly design**, for when you need to approve requests on the go.
- Granular permission system.
- Localization into other languages.
- Support for **PostgreSQL** and **SQLite** databases.
- Support for various notification agents.
- Easily **Watchlist** or **Blacklist** media.
- Support for PostgreSQL and SQLite databases.
- More features to come!
## Motivation
The primary motivation for starting Jellyseerr was to bring Jellyfin and Emby support to Overseerr. However, over time, **Jellyseerr** has evolved into its own distinct application with unique features. Designed as a one-stop shop for media requests, it offers a simple, easy-to-use experience for managing requests on Jellyfin, Emby, and Plex servers.
The primary motivation for starting this project was to add support for Jellyfin and Emby to Overseerr. As Overseerr is an incredibly performant and easy-to-use application, we wanted to bring that same experience to Jellyfin and Emby users. Thus, **Jellyseerr** was born.
This application is designed to be a **one-stop-shop** for all your media requests. It is designed to be a **simple, easy-to-use** application that allows users to request media to be added to your Jellyfin/Emby/Plex server.
## We need your help!
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is an ambitious project where developers/contributors poured a lot of work into, and that builds on top of [Overseerr](https://github.com/sct/overseerr). And we have a lot more to do as well.
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, with a heavy focus on Jellyfin and Emby integration.
[Overseerr](https://github.com/sct/overseerr) is an ambitious project where the original developers/contributors have already poured a lot of work into, and we wanted to build on top of that.
We value your feedback and support in identifying and fixing bugs to make Jellyseerr even better. As an open-source project, we welcome contributions from everyone. While Jellyseerr has diverged from Overseerr and evolved into its own unique application, we still encourage contributions to Overseerr, as it played a crucial role in inspiring what Jellyseerr has become today.
We also have poured a lot of work into this project, and we have a lot more to do as well. We need your valuable feedback and help to find and fix bugs. Also, with Jellyseerr being an open-source project, anyone is welcome to contribute. We also encourage you to contribute to Overseerr as well.
Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.

View File

@@ -1,21 +0,0 @@
---
title: Kubernetes
description: Install Jellyseerr in Kubernetes
sidebar_position: 5
---
# Kubernetes
:::info
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
:::
## Installation
```console
helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart
```
Helm values can be found in the Jellyseerr repository under [charts/jellyseerr-chart/README.md](https://github.com/Fallenbagel/jellyseerr/tree/develop/charts/jellyseerr-chart).
Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) :
```console
cosign verify ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart:[tag] --certificate-identity=https://github.com/Fallenbagel/jellyseerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.ac
tions.githubusercontent.com
```

View File

@@ -1,93 +0,0 @@
---
title: Backups
description: Understand which data you should back up.
sidebar_position: 4
---
# Which data does Jellyseerr save and where?
## Settings
All configurations from the **Settings** panel in the Jellyseerr web UI are saved, including integrations with Radarr, Sonarr, Jellyfin, Plex, and notification settings.
These settings are stored in the `settings.json` file located in the Jellyseerr data folder.
## User Data
Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL).
# Backup
### SQLite
If your backup system uses filesystem snapshots (such as Kubernetes with Volsync), you can directly back up the Jellyseerr data folder.
Otherwise, you need to stop the Jellyseerr application and back up the `config` folder.
For advanced users, it's possible to back up the database without stopping the application by using the [SQLite CLI](https://www.sqlite.org/download.html). Run the following command to create a backup:
```bash
sqlite3 db/db.sqlite3 ".backup '/tmp/jellyseerr_db.sqlite3.bak'"
```
Then, copy the `/tmp/jellyseerr_dump.sqlite3.bak` file to your desired backup location.
### PostgreSQL
You can back up the `config` folder and dump the PostgreSQL database without stopping the Jellyseerr application.
Install [postgresql-client](https://www.postgresql.org/download/) and run the following command to create a backup (just replace the placeholders):
:::info
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
-h, --host=HOSTNAME database server host or socket directory
-p, --port=PORT database server port number
:::
```bash
pg_dump -U <database_user> -d <database_name> -f /tmp/jellyseerr_db.sql
```
# Restore
### SQLite
After restoring your `db/db.sqlite3` file and, optionally, the `settings.json` file, the `config` folder structure should look like this:
```
.
├── cache <-- Optional
├── db
│ └── db.sqlite3
├── logs <-- Optional
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
```
Once the files are restored, start the Jellyseerr application.
### PostgreSQL
Install the [PostgreSQL client](https://www.postgresql.org/download/) and restore the PostgreSQL database using the following command (replace the placeholders accordingly):
:::info
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
-h, --host=HOSTNAME database server host or socket directory
-p, --port=PORT database server port number
:::
```bash
pg_restore -U <database_user> -d <database_name> /tmp/jellyseerr_db.sql
```
Optionally, restore the `settings.json` file. The `config` folder structure should look like this:
```
.
├── cache <-- Optional
├── logs <-- Optional
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
```
Once the database and files are restored, start the Jellyseerr application.

View File

@@ -14,14 +14,6 @@ When disabled, your mediaserver OAuth becomes the only sign-in option, and any "
This setting is **enabled** by default.
## Enable Jellyfin/Emby/Plex Sign-In
When enabled, users will be able to sign in to Jellyseerr using their Jellyfin/Emby/Plex credentials, provided they have linked their media server accounts.
When disabled, users will only be able to sign in using their email address. Users without a password set will not be able to sign in to Jellyseerr.
This setting is **enabled** by default.
## Enable New Jellyfin/Emby/Plex Sign-In
When enabled, users with access to your media server will be able to sign in to Jellyseerr even if they have not yet been imported. Users will be automatically assigned the permissions configured in the [Default Permissions](#default-permissions) setting upon first sign-in.

View File

@@ -11,7 +11,7 @@ const config: Config = {
baseUrl: '/',
trailingSlash: false,
organizationName: 'fallenbagel',
organizationName: 'Fallenbagel',
projectName: 'Jellyseerr',
deploymentBranch: 'gh-pages',
@@ -32,7 +32,7 @@ const config: Config = {
routeBasePath: '/',
path: '../docs',
editUrl:
'https://github.com/fallenbagel/jellyseerr/edit/develop/docs/',
'https://github.com/Fallenbagel/jellyseerr/edit/develop/docs/',
},
blog: false,
pages: false,
@@ -70,7 +70,7 @@ const config: Config = {
},
items: [
{
href: 'https://github.com/fallenbagel/jellyseerr',
href: 'https://github.com/Fallenbagel/jellyseerr',
label: 'GitHub',
position: 'right',
},

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -11,7 +11,6 @@ module.exports = {
{ hostname: 'gravatar.com' },
{ hostname: 'image.tmdb.org' },
{ hostname: 'artworks.thetvdb.com' },
{ hostname: 'plex.tv' },
],
},
webpack(config) {

View File

@@ -1,19 +1,19 @@
openapi: '3.0.2'
info:
title: 'Jellyseerr API'
title: 'Overseerr API'
version: '1.0.0'
description: |
This is the documentation for the Jellyseerr API backend.
This is the documentation for the Overseerr API backend.
Two primary authentication methods are supported:
- **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie.
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Jellyseerr.
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Overseerr.
tags:
- name: public
description: Public API endpoints requiring no authentication.
- name: settings
description: Endpoints related to Jellyseerr's settings and configuration.
description: Endpoints related to Overseerr's settings and configuration.
- name: auth
description: Endpoints related to logging in or out, and the currently authenticated user.
- name: users
@@ -160,10 +160,16 @@ components:
example: en
applicationTitle:
type: string
example: Jellyseerr
example: Overseerr
applicationUrl:
type: string
example: https://os.example.com
trustProxy:
type: boolean
example: true
csrfProtection:
type: boolean
example: false
hideAvailable:
type: boolean
example: false
@@ -185,18 +191,6 @@ components:
enableSpecialEpisodes:
type: boolean
example: false
NetworkSettings:
type: object
properties:
csrfProtection:
type: boolean
example: false
forceIpv4First:
type: boolean
example: false
trustProxy:
type: boolean
example: true
PlexLibrary:
type: object
properties:
@@ -1435,7 +1429,7 @@ components:
example: no-reply@example.com
senderName:
type: string
example: Jellyseerr
example: Overseerr
smtpHost:
type: string
example: 127.0.0.1
@@ -1966,8 +1960,8 @@ components:
paths:
/status:
get:
summary: Get Jellyseerr status
description: Returns the current Jellyseerr status in a JSON object.
summary: Get Overseerr status
description: Returns the current Overseerr status in a JSON object.
security: []
tags:
- public
@@ -2045,37 +2039,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
/settings/network:
get:
summary: Get network settings
description: Retrieves all network settings in a JSON object.
tags:
- settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
post:
summary: Update network settings
description: Updates network settings with the provided values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NetworkSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/NetworkSettings'
/settings/main/regenerate:
post:
summary: Get main settings with newly-generated API key
@@ -3812,11 +3775,6 @@ paths:
required: false
schema:
type: string
- in: query
name: includeIds
required: false
schema:
type: string
responses:
'200':
description: A JSON array of all users
@@ -4425,104 +4383,6 @@ paths:
responses:
'204':
description: User password updated
/user/{userId}/settings/linked-accounts/plex:
post:
summary: Link the provided Plex account to the current user
description: Logs in to Plex with the provided auth token, then links the associated Plex account with the user's account. Users can only link external accounts to their own account.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
authToken:
type: string
required:
- authToken
responses:
'204':
description: Linking account succeeded
'403':
description: Invalid credentials
'422':
description: Account already linked to a user
delete:
summary: Remove the linked Plex account for a user
description: Removes the linked Plex account for a specific user. Requires `MANAGE_USERS` permission if editing other users.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
responses:
'204':
description: Unlinking account succeeded
'400':
description: Unlink request invalid
'404':
description: User does not exist
/user/{userId}/settings/linked-accounts/jellyfin:
post:
summary: Link the provided Jellyfin account to the current user
description: Logs in to Jellyfin with the provided credentials, then links the associated Jellyfin account with the user's account. Users can only link external accounts to their own account.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
username:
type: string
example: 'Mr User'
password:
type: string
example: 'supersecret'
responses:
'204':
description: Linking account succeeded
'403':
description: Invalid credentials
'422':
description: Account already linked to a user
delete:
summary: Remove the linked Jellyfin account for a user
description: Removes the linked Jellyfin account for a specific user. Requires `MANAGE_USERS` permission if editing other users.
tags:
- users
parameters:
- in: path
name: userId
required: true
schema:
type: number
responses:
'204':
description: Unlinking account succeeded
'400':
description: Unlink request invalid
'404':
description: User does not exist
/user/{userId}/settings/notifications:
get:
summary: Get notification settings for a user

View File

@@ -5,7 +5,7 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "node postinstall-win.js",
"dev": "nodemon -e ts --watch server --watch jellyseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"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",
"build": "pnpm build:next && pnpm build:server",
@@ -32,7 +32,6 @@
},
"license": "MIT",
"dependencies": {
"@dr.pogodin/csurf": "^1.14.1",
"@formatjs/intl-displaynames": "6.2.6",
"@formatjs/intl-locale": "3.1.1",
"@formatjs/intl-pluralrules": "5.1.10",
@@ -43,20 +42,20 @@
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.30",
"@types/wink-jaro-distance": "^2.0.2",
"ace-builds": "1.15.2",
"bcrypt": "5.1.0",
"bowser": "2.11.0",
"connect-typeorm": "1.1.4",
"cookie-parser": "1.4.7",
"cookie-parser": "1.4.6",
"copy-to-clipboard": "3.3.3",
"country-flag-icons": "1.5.5",
"cronstrue": "2.23.0",
"csurf": "1.11.0",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"email-templates": "12.0.1",
"email-templates": "9.0.0",
"email-validator": "2.0.4",
"express": "4.21.2",
"express": "4.18.2",
"express-openapi-validator": "4.13.8",
"express-rate-limit": "6.7.0",
"express-session": "1.17.3",
@@ -64,15 +63,15 @@
"gravatar-url": "3.1.0",
"lodash": "4.17.21",
"mime": "3",
"next": "^14.2.24",
"next": "^14.2.4",
"node-cache": "5.1.2",
"node-gyp": "9.3.1",
"node-schedule": "2.1.1",
"nodemailer": "6.10.0",
"openpgp": "5.11.2",
"nodemailer": "6.9.1",
"openpgp": "5.7.0",
"pg": "8.11.0",
"plex-api": "5.3.2",
"pug": "3.0.3",
"pug": "3.0.2",
"react": "^18.3.1",
"react-ace": "10.1.0",
"react-animate-height": "2.1.2",
@@ -86,27 +85,24 @@
"react-spring": "9.7.1",
"react-tailwindcss-datepicker-sct": "1.3.4",
"react-toast-notifications": "2.5.1",
"react-transition-group": "^4.4.5",
"react-truncate-markup": "5.1.2",
"react-use-clipboard": "1.0.9",
"reflect-metadata": "0.1.13",
"secure-random-password": "0.2.3",
"semver": "7.7.1",
"semver": "7.3.8",
"sharp": "^0.33.4",
"sqlite3": "5.1.7",
"sqlite3": "5.1.4",
"swagger-ui-express": "4.6.2",
"swr": "2.2.5",
"tailwind-merge": "^2.6.0",
"typeorm": "0.3.11",
"undici": "^7.3.0",
"undici": "^6.20.1",
"web-push": "3.5.0",
"wink-jaro-distance": "^2.0.0",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1",
"xml2js": "0.4.23",
"yamljs": "0.3.0",
"yup": "0.32.11",
"zod": "3.24.2"
"zod": "3.20.6"
},
"devDependencies": {
"@commitlint/cli": "17.4.4",
@@ -116,8 +112,8 @@
"@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1",
"@tailwindcss/aspect-ratio": "0.4.2",
"@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16",
"@tailwindcss/forms": "0.5.3",
"@tailwindcss/typography": "0.5.9",
"@types/bcrypt": "5.0.0",
"@types/cookie-parser": "1.4.3",
"@types/country-flag-icons": "1.2.0",
@@ -146,7 +142,7 @@
"commitizen": "4.3.0",
"copyfiles": "2.4.1",
"cy-mobile-commands": "0.3.0",
"cypress": "14.1.0",
"cypress": "12.7.0",
"cz-conventional-changelog": "3.3.0",
"eslint": "8.35.0",
"eslint-config-next": "^14.2.4",
@@ -159,8 +155,8 @@
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"lint-staged": "13.1.2",
"nodemon": "3.1.9",
"postcss": "8.4.31",
"nodemon": "2.0.20",
"postcss": "8.4.21",
"prettier": "2.8.4",
"prettier-plugin-organize-imports": "3.2.2",
"prettier-plugin-tailwindcss": "0.2.3",
@@ -239,8 +235,7 @@
"COMMIT_TAG": "$GIT_SHA"
},
"imageNames": [
"fallenbagel/jellyseerr",
"ghcr.io/fallenbagel/jellyseerr"
"fallenbagel/jellyseerr"
],
"platforms": [
"linux/amd64",

2809
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -3,7 +3,7 @@
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const OFFLINE_VERSION = 4;
const OFFLINE_VERSION = 3;
const CACHE_NAME = 'offline';
// Customize this with a different URL if needed.
const OFFLINE_URL = '/offline.html';
@@ -107,25 +107,6 @@ self.addEventListener('push', (event) => {
);
}
// Set the badge with the amount of pending requests
// Only update the badge if the payload confirms they are the admin
if (
(payload.notificationType === 'MEDIA_APPROVED' ||
payload.notificationType === 'MEDIA_DECLINED') &&
payload.isAdmin
) {
if ('setAppBadge' in navigator) {
navigator.setAppBadge(payload.pendingRequestsCount);
}
return;
}
if (payload.notificationType === 'MEDIA_PENDING') {
if ('setAppBadge' in navigator) {
navigator.setAppBadge(payload.pendingRequestsCount);
}
}
event.waitUntil(self.registration.showNotification(payload.subject, options));
});

View File

@@ -72,7 +72,7 @@ class GithubAPI extends ExternalAPI {
);
}
public async getJellyseerrReleases({
public async getOverseerrReleases({
take = 20,
}: {
take?: number;
@@ -88,14 +88,14 @@ class GithubAPI extends ExternalAPI {
return data;
} catch (e) {
logger.warn(
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];
}
}
public async getJellyseerrCommits({
public async getOverseerrCommits({
take = 20,
branch = 'develop',
}: {
@@ -114,7 +114,7 @@ class GithubAPI extends ExternalAPI {
return data;
} catch (e) {
logger.warn(
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
{ label: 'GitHub API', errorMessage: e.message }
);
return [];

View File

@@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ExternalAPI from '@server/api/externalapi';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import availabilitySync from '@server/lib/availabilitySync';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { ApiError } from '@server/types/error';
import { getAppVersion } from '@server/utils/appVersion';
@@ -94,22 +92,10 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
DateCreated?: string;
}
export interface JellyfinItemsReponse {
Items: JellyfinLibraryItemExtended[];
TotalRecordCount: number;
StartIndex: number;
}
class JellyfinAPI extends ExternalAPI {
private userId?: string;
private mediaServerType: MediaServerType;
constructor(
jellyfinHost: string,
authToken?: string | null,
deviceId?: string | null
) {
const settings = getSettings();
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
let authHeaderVal: string;
if (authToken) {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
@@ -126,8 +112,6 @@ class JellyfinAPI extends ExternalAPI {
},
}
);
this.mediaServerType = settings.main.mediaServerType;
}
public async login(
@@ -308,15 +292,18 @@ class JellyfinAPI extends ExternalAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try {
const libraryItemsResponse = await this.get<any>(`/Items`, {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Series,Movie,Others',
Recursive: 'true',
StartIndex: '0',
ParentId: id,
collapseBoxSetItems: 'false',
});
const libraryItemsResponse = await this.get<any>(
`/Users/${this.userId}/Items`,
{
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Series,Movie,Others',
Recursive: 'true',
StartIndex: '0',
ParentId: id,
collapseBoxSetItems: 'false',
}
);
return libraryItemsResponse.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
@@ -333,22 +320,13 @@ class JellyfinAPI extends ExternalAPI {
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
try {
const endpoint =
this.mediaServerType === MediaServerType.JELLYFIN
? `/Items/Latest`
: `/Users/${this.userId}/Items/Latest`;
const baseParams = {
Limit: '12',
ParentId: id,
};
const params =
this.mediaServerType === MediaServerType.JELLYFIN
? { ...baseParams, userId: this.userId ?? `Me` }
: baseParams;
const itemResponse = await this.get<any>(endpoint, params);
const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/Latest`,
{
Limit: '12',
ParentId: id,
}
);
return itemResponse;
} catch (e) {
@@ -365,12 +343,11 @@ class JellyfinAPI extends ExternalAPI {
id: string
): Promise<JellyfinLibraryItemExtended | undefined> {
try {
const itemResponse = await this.get<JellyfinItemsReponse>(`/Items`, {
ids: id,
fields: 'ProviderIds,MediaSources,Width,Height,IsHD,DateCreated',
});
const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/${id}`
);
return itemResponse.Items?.[0];
return itemResponse;
} catch (e) {
if (availabilitySync.running) {
if (e.cause?.status === 500) {

View File

@@ -92,7 +92,7 @@ class PlexAPI {
plexSettings,
timeout,
}: {
plexToken?: string | null;
plexToken?: string;
plexSettings?: PlexSettings;
timeout?: number;
}) {
@@ -107,7 +107,7 @@ class PlexAPI {
port: settingsPlex.port,
https: settingsPlex.useSsl,
timeout: timeout,
token: plexToken ?? undefined,
token: plexToken,
authenticator: {
authenticate: (
_plexApi,
@@ -124,9 +124,9 @@ class PlexAPI {
// },
options: {
identifier: settings.clientId,
product: 'Jellyseerr',
deviceName: 'Jellyseerr',
platform: 'Jellyseerr',
product: 'Overseerr',
deviceName: 'Overseerr',
platform: 'Overseerr',
},
});
}

View File

@@ -1,7 +1,6 @@
import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import { getSettings } from '@server/lib/settings';
import jaro from 'wink-jaro-distance';
interface RTAlgoliaSearchResponse {
results: {
@@ -16,7 +15,7 @@ interface RTAlgoliaHit {
tmsId: string;
type: string;
title: string;
titles?: string[];
titles: string[];
description: string;
releaseYear: number;
rating: string;
@@ -25,9 +24,9 @@ interface RTAlgoliaHit {
isEmsSearchable: boolean;
rtId: number;
vanity: string;
aka?: string[];
aka: string[];
posterImageUrl: string;
rottenTomatoes?: {
rottenTomatoes: {
audienceScore: number;
criticsIconUrl: string;
wantToSeeCount: number;
@@ -48,47 +47,6 @@ export interface RTRating {
url: string;
}
// Tunables
const INEXACT_TITLE_FACTOR = 0.25;
const ALTERNATE_TITLE_FACTOR = 0.8;
const PER_YEAR_PENALTY = 0.4;
const MINIMUM_SCORE = 0.175;
// Normalization for title comparisons.
// Lowercase and strip non-alphanumeric (unicode-aware).
const norm = (s: string): string =>
s.toLowerCase().replace(/[^\p{L}\p{N} ]/gu, '');
// Title similarity. 1 if exact, quarter-jaro otherwise.
const similarity = (a: string, b: string): number =>
a === b ? 1 : jaro(a, b).similarity * INEXACT_TITLE_FACTOR;
// Gets the best similarity score between the searched title and all alternate
// titles of the search result. Non-main titles are penalized.
const t_score = ({ title, titles, aka }: RTAlgoliaHit, s: string): number => {
const f = (t: string, i: number) =>
similarity(norm(t), norm(s)) * (i ? ALTERNATE_TITLE_FACTOR : 1);
return Math.max(...[title].concat(aka || [], titles || []).map(f));
};
// Year difference to score: 0 -> 1.0, 1 -> 0.6, 2 -> 0.2, 3+ -> 0.0
const y_score = (r: RTAlgoliaHit, y?: number): number =>
y ? Math.max(0, 1 - Math.abs(r.releaseYear - y) * PER_YEAR_PENALTY) : 1;
// Cut score in half if result has no ratings.
const extra_score = (r: RTAlgoliaHit): number => (r.rottenTomatoes ? 1 : 0.5);
// Score search result as product of all subscores
const score = (r: RTAlgoliaHit, name: string, year?: number): number =>
t_score(r, name) * y_score(r, year) * extra_score(r);
// Score each search result and return the highest scoring result, if any
const best = (rs: RTAlgoliaHit[], name: string, year?: number): RTAlgoliaHit =>
rs
.map((r) => ({ score: score(r, name, year), result: r }))
.filter(({ score }) => score > MINIMUM_SCORE)
.sort(({ score: a }, { score: b }) => b - a)[0]?.result;
/**
* This is a best-effort API. The Rotten Tomatoes API is technically
* private and getting access costs money/requires approval.
@@ -132,21 +90,47 @@ class RottenTomatoes extends ExternalAPI {
year: number
): Promise<RTRating | null> {
try {
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"movie"');
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
requests: [
{
indexName: 'content_rt',
query: name.replace(/\bthe\b ?/gi, ''),
params: `filters=${filters}&hitsPerPage=20`,
query: name,
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
},
],
});
const contentResults = data.results.find((r) => r.index === 'content_rt');
const movie = best(contentResults?.hits || [], name, year);
if (!movie?.rottenTomatoes) return null;
if (!contentResults) {
return null;
}
// First, attempt to match exact name and year
let movie = contentResults.hits.find(
(movie) => movie.releaseYear === year && movie.title === name
);
// If we don't find a movie, try to match partial name and year
if (!movie) {
movie = contentResults.hits.find(
(movie) => movie.releaseYear === year && movie.title.includes(name)
);
}
// If we still dont find a movie, try to match just on year
if (!movie) {
movie = contentResults.hits.find((movie) => movie.releaseYear === year);
}
// One last try, try exact name match only
if (!movie) {
movie = contentResults.hits.find((movie) => movie.title === name);
}
if (!movie?.rottenTomatoes) {
return null;
}
return {
title: movie.title,
@@ -174,21 +158,33 @@ class RottenTomatoes extends ExternalAPI {
year?: number
): Promise<RTRating | null> {
try {
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"tv"');
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
requests: [
{
indexName: 'content_rt',
query: name,
params: `filters=${filters}&hitsPerPage=20`,
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
},
],
});
const contentResults = data.results.find((r) => r.index === 'content_rt');
const tvshow = best(contentResults?.hits || [], name, year);
if (!tvshow?.rottenTomatoes) return null;
if (!contentResults) {
return null;
}
let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0];
if (year) {
tvshow = contentResults.hits.find(
(series) => series.releaseYear === year
);
}
if (!tvshow || !tvshow.rottenTomatoes) {
return null;
}
return {
title: tvshow.title,

View File

@@ -108,7 +108,7 @@ class TheMovieDb extends ExternalAPI {
super(
'https://api.themoviedb.org/3',
{
api_key: '431a8708161bcd1f1fbe7536137e61ed',
api_key: 'db55323b8d3e4154498498a75642b381',
},
{
nodeCache: cacheManager.getCache('tmdb').data,
@@ -256,7 +256,6 @@ class TheMovieDb extends ExternalAPI {
language,
append_to_response:
'credits,external_ids,videos,keywords,release_dates,watch/providers',
include_video_language: language + ', en',
},
43200
);
@@ -281,7 +280,6 @@ class TheMovieDb extends ExternalAPI {
language,
append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
include_video_language: language + ', en',
},
43200
);

View File

@@ -7,6 +7,5 @@ export enum ApiErrorCode {
NoAdminUser = 'NO_ADMIN_USER',
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
Unauthorized = 'UNAUTHORIZED',
Unknown = 'UNKNOWN',
}

View File

@@ -734,11 +734,8 @@ export class MediaRequest {
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
const statusField = this.is4k ? 'status4k' : 'status';
await mediaRepository.update(
{ id: this.media.id },
{ [statusField]: MediaStatus.UNKNOWN }
);
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
/**
@@ -755,11 +752,8 @@ export class MediaRequest {
).length === 0 &&
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
) {
const statusField = this.is4k ? 'status4k' : 'status';
mediaRepository.update(
{ id: this.media.id },
{ [statusField]: MediaStatus.UNKNOWN }
);
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
// Approve child seasons if parent is approved
@@ -961,10 +955,8 @@ export class MediaRequest {
});
const requestRepository = getRepository(MediaRequest);
await requestRepository.update(this.id, {
status: MediaRequestStatus.APPROVED,
});
this.status = MediaRequestStatus.APPROVED;
await requestRepository.save(this);
return;
}
@@ -994,22 +986,18 @@ export class MediaRequest {
throw new Error('Media data not found');
}
const updateFields = {
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
radarrMovie.id,
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
radarrMovie.titleSlug,
[this.is4k ? 'serviceId4k' : 'serviceId']: radarrMovie?.id,
};
await mediaRepository.update({ id: this.media.id }, updateFields);
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
radarrMovie.id;
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
radarrMovie.titleSlug;
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
await mediaRepository.save(media);
})
.catch(async () => {
const requestRepository = getRepository(MediaRequest);
await requestRepository.update(this.id, {
status: MediaRequestStatus.FAILED,
});
this.status = MediaRequestStatus.FAILED;
await requestRepository.save(this);
logger.warn(
'Something went wrong sending movie request to Radarr, marking status as FAILED',
@@ -1125,9 +1113,8 @@ export class MediaRequest {
});
const requestRepository = getRepository(MediaRequest);
await requestRepository.update(this.id, {
status: MediaRequestStatus.APPROVED,
});
this.status = MediaRequestStatus.APPROVED;
await requestRepository.save(this);
return;
}

View File

@@ -56,11 +56,11 @@ export class User {
})
public email: string;
@Column({ type: 'varchar', nullable: true })
public plexUsername?: string | null;
@Column({ nullable: true })
public plexUsername?: string;
@Column({ type: 'varchar', nullable: true })
public jellyfinUsername?: string | null;
@Column({ nullable: true })
public jellyfinUsername?: string;
@Column({ nullable: true })
public username?: string;
@@ -77,20 +77,20 @@ export class User {
@Column({ type: 'integer', default: UserType.PLEX })
public userType: UserType;
@Column({ type: 'integer', nullable: true, select: true })
public plexId?: number | null;
@Column({ nullable: true, select: true })
public plexId?: number;
@Column({ type: 'varchar', nullable: true })
public jellyfinUserId?: string | null;
@Column({ nullable: true })
public jellyfinUserId?: string;
@Column({ type: 'varchar', nullable: true, select: false })
public jellyfinDeviceId?: string | null;
@Column({ nullable: true })
public jellyfinDeviceId?: string;
@Column({ type: 'varchar', nullable: true, select: false })
public jellyfinAuthToken?: string | null;
@Column({ nullable: true })
public jellyfinAuthToken?: string;
@Column({ type: 'varchar', nullable: true, select: false })
public plexToken?: string | null;
@Column({ nullable: true })
public plexToken?: string;
@Column({ type: 'integer', default: 0 })
public permissions = 0;

View File

@@ -1,4 +1,3 @@
import csurf from '@dr.pogodin/csurf';
import PlexAPI from '@server/api/plexapi';
import dataSource, { getRepository, isPgsql } from '@server/datasource';
import DiscoverSlider from '@server/entity/DiscoverSlider';
@@ -29,6 +28,7 @@ import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import type { NextFunction, Request, Response } from 'express';
import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
@@ -41,9 +41,14 @@ import path from 'path';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
const API_SPEC_PATH = path.join(__dirname, '../jellyseerr-api.yml');
if (process.env.forceIpv4First === 'true') {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
logger.info(`Starting Jellyseerr version ${getAppVersion()}`);
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
@@ -72,20 +77,11 @@ app
// Load Settings
const settings = await getSettings().load();
restartFlag.initializeSettings(settings);
// Check if we force IPv4 first
if (
process.env.forceIpv4First === 'true' ||
settings.network.forceIpv4First
) {
dns.setDefaultResultOrder('ipv4first');
net.setDefaultAutoSelectFamily(false);
}
restartFlag.initializeSettings(settings.main);
// Register HTTP proxy
if (settings.network.proxy.enabled) {
await createCustomProxyAgent(settings.network.proxy);
if (settings.main.proxy.enabled) {
await createCustomProxyAgent(settings.main.proxy);
}
// Migrate library types
@@ -140,7 +136,7 @@ app
await DiscoverSlider.bootstrapSliders();
const server = express();
if (settings.network.trustProxy) {
if (settings.main.trustProxy) {
server.enable('trust proxy');
}
server.use(cookieParser());
@@ -161,7 +157,7 @@ app
next();
}
});
if (settings.network.csrfProtection) {
if (settings.main.csrfProtection) {
server.use(
csurf({
cookie: {
@@ -191,7 +187,7 @@ app
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30,
httpOnly: true,
sameSite: settings.network.csrfProtection ? 'strict' : 'lax',
sameSite: settings.main.csrfProtection ? 'strict' : 'lax',
secure: 'auto',
},
store: new TypeormStore({

View File

@@ -30,7 +30,6 @@ export interface PublicSettingsResponse {
applicationUrl: string;
hideAvailable: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
discoverRegion: string;

View File

@@ -70,35 +70,6 @@ export const startJobs = (): void => {
running: () => plexFullScanner.status().running,
cancelFn: () => plexFullScanner.cancel(),
});
scheduledJobs.push({
id: 'plex-refresh-token',
name: 'Plex Refresh Token',
type: 'process',
interval: 'fixed',
cronSchedule: jobs['plex-refresh-token'].schedule,
job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => {
logger.info('Starting scheduled job: Plex Refresh Token', {
label: 'Jobs',
});
refreshToken.run();
}),
});
// Watchlist Sync
scheduledJobs.push({
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'seconds',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
label: 'Jobs',
});
watchlistSync.syncWatchlist();
}),
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
@@ -141,6 +112,21 @@ export const startJobs = (): void => {
});
}
// Watchlist Sync
scheduledJobs.push({
id: 'plex-watchlist-sync',
name: 'Plex Watchlist Sync',
type: 'process',
interval: 'seconds',
cronSchedule: jobs['plex-watchlist-sync'].schedule,
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
logger.info('Starting scheduled job: Plex Watchlist Sync', {
label: 'Jobs',
});
watchlistSync.syncWatchlist();
}),
});
// Run full radarr scan every 24 hours
scheduledJobs.push({
id: 'radarr-scan',
@@ -237,5 +223,19 @@ export const startJobs = (): void => {
}),
});
scheduledJobs.push({
id: 'plex-refresh-token',
name: 'Plex Refresh Token',
type: 'process',
interval: 'fixed',
cronSchedule: jobs['plex-refresh-token'].schedule,
job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => {
logger.info('Starting scheduled job: Plex Refresh Token', {
label: 'Jobs',
});
refreshToken.run();
}),
});
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
};

View File

@@ -50,7 +50,6 @@ class PreparedEmail extends Email {
},
send: true,
transport: transport,
preview: false,
});
}
}

View File

@@ -19,8 +19,6 @@ export interface NotificationPayload {
request?: MediaRequest;
issue?: Issue;
comment?: IssueComment;
pendingRequestsCount?: number;
isAdmin?: boolean;
}
export abstract class BaseAgent<T extends NotificationAgentConfig> {

View File

@@ -188,7 +188,7 @@ class SlackAgent
type: 'actions',
elements: [
{
action_id: 'open-in-jellyseerr',
action_id: 'open-in-overseerr',
type: 'button',
url,
text: {

View File

@@ -1,7 +1,6 @@
import { IssueType, IssueTypeName } from '@server/constants/issue';
import { MediaRequestStatus, MediaType } from '@server/constants/media';
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import MediaRequest from '@server/entity/MediaRequest';
import { User } from '@server/entity/User';
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
import type { NotificationAgentConfig } from '@server/lib/settings';
@@ -20,8 +19,6 @@ interface PushNotificationPayload {
actionUrl?: string;
actionUrlTitle?: string;
requestId?: number;
pendingRequestsCount?: number;
isAdmin?: boolean;
}
class WebPushAgent
@@ -132,8 +129,6 @@ class WebPushAgent
requestId: payload.request?.id,
actionUrl,
actionUrlTitle,
pendingRequestsCount: payload.pendingRequestsCount,
isAdmin: payload.isAdmin,
};
}
@@ -157,51 +152,6 @@ class WebPushAgent
const mainUser = await userRepository.findOne({ where: { id: 1 } });
const requestRepository = getRepository(MediaRequest);
const pendingRequests = await requestRepository.find({
where: { status: MediaRequestStatus.PENDING },
});
const webPushNotification = async (
pushSub: UserPushSubscription,
notificationPayload: Buffer
) => {
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: pushSub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
endpoint: pushSub.endpoint,
keys: {
auth: pushSub.auth,
p256dh: pushSub.p256dh,
},
},
notificationPayload
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: pushSub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(pushSub);
}
};
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
@@ -219,11 +169,7 @@ class WebPushAgent
pushSubs.push(...notifySubs);
}
if (
payload.notifyAdmin ||
type === Notification.MEDIA_APPROVED ||
type === Notification.MEDIA_DECLINED
) {
if (payload.notifyAdmin) {
const users = await userRepository.find();
const manageUsers = users.filter(
@@ -246,42 +192,7 @@ class WebPushAgent
})
.getMany();
// We only want to send the custom notification when type is approved or declined
// Otherwise, default to the normal notification
if (
type === Notification.MEDIA_APPROVED ||
type === Notification.MEDIA_DECLINED
) {
if (mainUser && allSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
// Custom payload only for updating the app badge
const notificationBadgePayload = Buffer.from(
JSON.stringify(
this.getNotificationPayload(type, {
subject: payload.subject,
notifySystem: false,
notifyAdmin: true,
isAdmin: true,
pendingRequestsCount: pendingRequests.length,
})
),
'utf-8'
);
await Promise.all(
allSubs.map(async (sub) => {
webPushNotification(sub, notificationBadgePayload);
})
);
}
} else {
pushSubs.push(...allSubs);
}
pushSubs.push(...allSubs);
}
if (mainUser && pushSubs.length > 0) {
@@ -291,10 +202,6 @@ class WebPushAgent
settings.vapidPrivate
);
if (type === Notification.MEDIA_PENDING) {
payload = { ...payload, pendingRequestsCount: pendingRequests.length };
}
const notificationPayload = Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
@@ -302,7 +209,39 @@ class WebPushAgent
await Promise.all(
pushSubs.map(async (sub) => {
webPushNotification(sub, notificationPayload);
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
notificationPayload
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}

View File

@@ -115,6 +115,7 @@ export interface MainSettings {
apiKey: string;
applicationTitle: string;
applicationUrl: string;
csrfProtection: boolean;
cacheImages: boolean;
defaultPermissions: number;
defaultQuotas: {
@@ -123,21 +124,15 @@ export interface MainSettings {
};
hideAvailable: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
newPlexLogin: boolean;
discoverRegion: string;
streamingRegion: string;
originalLanguage: string;
trustProxy: boolean;
mediaServerType: number;
partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean;
locale: string;
}
export interface NetworkSettings {
csrfProtection: boolean;
forceIpv4First: boolean;
trustProxy: boolean;
proxy: ProxySettings;
}
@@ -150,7 +145,6 @@ interface FullPublicSettings extends PublicSettings {
applicationUrl: string;
hideAvailable: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
discoverRegion: string;
@@ -317,7 +311,6 @@ export interface AllSettings {
public: PublicSettings;
notifications: NotificationSettings;
jobs: Record<JobId, JobSettings>;
network: NetworkSettings;
}
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
@@ -336,6 +329,7 @@ class Settings {
apiKey: '',
applicationTitle: 'Jellyseerr',
applicationUrl: '',
csrfProtection: false,
cacheImages: false,
defaultPermissions: Permission.REQUEST,
defaultQuotas: {
@@ -344,15 +338,25 @@ class Settings {
},
hideAvailable: false,
localLogin: true,
mediaServerLogin: true,
newPlexLogin: true,
discoverRegion: '',
streamingRegion: '',
originalLanguage: '',
trustProxy: false,
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,
enableSpecialEpisodes: false,
locale: 'en',
proxy: {
enabled: false,
hostname: '',
port: 8080,
useSsl: false,
user: '',
password: '',
bypassFilter: '',
bypassLocalAddresses: true,
},
},
plex: {
name: '',
@@ -505,21 +509,6 @@ class Settings {
schedule: '0 0 5 * * *',
},
},
network: {
csrfProtection: false,
trustProxy: false,
forceIpv4First: false,
proxy: {
enabled: false,
hostname: '',
port: 8080,
useSsl: false,
user: '',
password: '',
bypassFilter: '',
bypassLocalAddresses: true,
},
},
};
if (initialSettings) {
this.data = merge(this.data, initialSettings);
@@ -589,8 +578,6 @@ class Settings {
applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable,
localLogin: this.data.main.localLogin,
mediaServerLogin: this.data.main.mediaServerLogin,
jellyfinExternalHost: this.data.jellyfin.externalHostname,
jellyfinForgotPasswordUrl: this.data.jellyfin.jellyfinForgotPasswordUrl,
movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault
@@ -631,14 +618,6 @@ class Settings {
this.data.jobs = data;
}
get network(): NetworkSettings {
return this.data.network;
}
set network(data: NetworkSettings) {
this.data.network = data;
}
get clientId(): string {
return this.data.clientId;
}

View File

@@ -1,13 +1,6 @@
import type { AllSettings } from '@server/lib/settings';
const migrateRegionSetting = (settings: any): AllSettings => {
if (
settings.main.discoverRegion !== undefined &&
settings.main.streamingRegion !== undefined
) {
return settings;
}
const oldRegion = settings.main.region;
if (oldRegion) {
settings.main.discoverRegion = oldRegion;

View File

@@ -1,31 +0,0 @@
import type { AllSettings } from '@server/lib/settings';
const migrateNetworkSettings = (settings: any): AllSettings => {
if (settings.network) {
return settings;
}
const newSettings = { ...settings };
newSettings.network = {
...settings.network,
csrfProtection: settings.main.csrfProtection ?? false,
trustProxy: settings.main.trustProxy ?? false,
forceIpv4First: settings.main.forceIpv4First ?? false,
proxy: settings.main.proxy ?? {
enabled: false,
hostname: '',
port: 8080,
useSsl: false,
user: '',
password: '',
bypassFilter: '',
bypassLocalAddresses: true,
},
};
delete settings.main.csrfProtection;
delete settings.main.trustProxy;
delete settings.main.forceIpv4First;
delete settings.main.proxy;
return newSettings;
};
export default migrateNetworkSettings;

View File

@@ -130,7 +130,7 @@ class WatchlistSync {
switch (e.constructor) {
// During watchlist sync, these errors aren't necessarily
// a problem with Jellyseerr. Since we are auto syncing these constantly, it's
// a problem with Overseerr. Since we are auto syncing these constantly, it's
// possible they are unexpectedly at their quota limit, for example. So we'll
// instead log these as debug messages.
case RequestPermissionError:

View File

@@ -43,14 +43,14 @@ const logger = winston.createLogger({
}),
new winston.transports.DailyRotateFile({
filename: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/logs/jellyseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/jellyseerr-%DATE%.log'),
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
createSymlink: true,
symlinkName: 'jellyseerr.log',
symlinkName: 'overseerr.log',
}),
new winston.transports.DailyRotateFile({
filename: process.env.CONFIG_DIRECTORY

View File

@@ -56,9 +56,8 @@ authRoutes.post('/plex', async (req, res, next) => {
}
if (
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED &&
(settings.main.mediaServerLogin === false ||
settings.main.mediaServerType != MediaServerType.PLEX)
settings.main.mediaServerType != MediaServerType.PLEX &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
) {
return res.status(500).json({ error: 'Plex login is disabled' });
}
@@ -158,7 +157,7 @@ authRoutes.post('/plex', async (req, res, next) => {
});
} else {
logger.info(
'Sign-in attempt from Plex user with access to the media server; creating new Jellyseerr user',
'Sign-in attempt from Plex user with access to the media server; creating new Overseerr user',
{
label: 'API',
ip: req.ip,
@@ -232,13 +231,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
//Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured
if (
// media server not configured, allow login for setup
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED &&
(settings.main.mediaServerLogin === false ||
// media server is neither jellyfin or emby
(settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY &&
settings.jellyfin.ip !== ''))
settings.jellyfin.ip !== ''
) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
}
@@ -267,14 +263,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
let user = await userRepository.findOne({
where: { jellyfinUsername: body.username },
select: { id: true, jellyfinDeviceId: true },
});
let deviceId = '';
if (user) {
deviceId = user.jellyfinDeviceId ?? '';
} else {
deviceId = Buffer.from(`BOT_jellyseerr_${body.username ?? ''}`).toString(
deviceId = Buffer.from(`BOT_overseerr_${body.username ?? ''}`).toString(
'base64'
);
}
@@ -446,7 +441,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
});
} else if (!user) {
logger.info(
'Sign-in attempt from Jellyfin user with access to the media server; creating new Jellyseerr user',
'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user',
{
label: 'API',
ip: req.ip,
@@ -584,7 +579,7 @@ authRoutes.post('/local', async (req, res, next) => {
.getOne();
if (!user || !(await user.passwordMatch(body.password))) {
logger.warn('Failed sign-in attempt using invalid Jellyseerr password', {
logger.warn('Failed sign-in attempt using invalid Overseerr password', {
label: 'API',
ip: req.ip,
email: body.email,
@@ -674,7 +669,7 @@ authRoutes.post('/local', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
logger.error(
'Something went wrong authenticating with Jellyseerr password',
'Something went wrong authenticating with Overseerr password',
{
label: 'API',
errorMessage: e.message,

View File

@@ -837,8 +837,7 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
select: ['id', 'plexToken'],
});
if (activeUser && !activeUser?.plexToken) {
// Non-Plex users can only see their own watchlist
if (activeUser) {
const [result, total] = await getRepository(Watchlist).findAndCount({
where: { requestedBy: { id: activeUser?.id } },
relations: {
@@ -867,7 +866,6 @@ discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
});
}
// List watchlist from Plex
const plexTV = new PlexTvAPI(activeUser.plexToken);
const watchlist = await plexTV.getWatchlist({ offset });

View File

@@ -55,7 +55,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
let commitsBehind = 0;
if (currentVersion.startsWith('develop-') && commitTag !== 'local') {
const commits = await githubApi.getJellyseerrCommits();
const commits = await githubApi.getOverseerrCommits();
if (commits.length) {
const filteredCommits = commits.filter(
@@ -74,7 +74,7 @@ router.get<unknown, StatusResponse>('/status', async (req, res) => {
}
}
} else if (commitTag !== 'local') {
const releases = await githubApi.getJellyseerrReleases();
const releases = await githubApi.getOverseerrReleases();
if (releases.length) {
const latestVersion = releases[0];
@@ -403,7 +403,7 @@ router.get('/watchproviders/tv', async (req, res, next) => {
router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Jellyseerr API',
api: 'Overseerr API',
version: '1.0',
});
});

View File

@@ -78,21 +78,6 @@ settingsRoutes.post('/main', async (req, res) => {
return res.status(200).json(settings.main);
});
settingsRoutes.get('/network', (req, res) => {
const settings = getSettings();
res.status(200).json(settings.network);
});
settingsRoutes.post('/network', async (req, res) => {
const settings = getSettings();
settings.network = merge(settings.network, req.body);
await settings.save();
return res.status(200).json(settings.network);
});
settingsRoutes.post('/main/regenerate', async (req, res, next) => {
const settings = getSettings();
@@ -352,7 +337,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
const account = await jellyfinClient.getUser();
// Automatic Library grouping is not supported when user views are used to get library
if (account.Configuration.GroupedFolders?.length > 0) {
if (account.Configuration.GroupedFolders.length > 0) {
return next({
status: 501,
message: ApiErrorCode.SyncErrorGroupedFolders,

View File

@@ -32,14 +32,7 @@ const router = Router();
router.get('/', async (req, res, next) => {
try {
const includeIds = [
...new Set(
req.query.includeIds ? req.query.includeIds.toString().split(',') : []
),
];
const pageSize = req.query.take
? Number(req.query.take)
: Math.max(10, includeIds.length);
const pageSize = req.query.take ? Number(req.query.take) : 10;
const skip = req.query.skip ? Number(req.query.skip) : 0;
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
let query = getRepository(User).createQueryBuilder('user');
@@ -51,10 +44,6 @@ router.get('/', async (req, res, next) => {
);
}
if (includeIds.length > 0) {
query.andWhereInIds(includeIds);
}
switch (req.query.sort) {
case 'updated':
query = query.orderBy('user.updatedAt', 'DESC');
@@ -95,7 +84,6 @@ router.get('/', async (req, res, next) => {
const [users, userCount] = await query
.take(pageSize)
.skip(skip)
.distinct(true)
.getManyAndCount();
return res.status(200).json({

View File

@@ -1,7 +1,4 @@
import JellyfinAPI from '@server/api/jellyfin';
import PlexTvAPI from '@server/api/plextv';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType } from '@server/constants/server';
import { UserType } from '@server/constants/user';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
@@ -15,23 +12,9 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
import { ApiError } from '@server/types/error';
import { getHostname } from '@server/utils/getHostname';
import { Router } from 'express';
import net from 'net';
import { canMakePermissionsChange } from '.';
const isOwnProfile = (): Middleware => {
return (req, res, next) => {
if (req.user?.id !== Number(req.params.id)) {
return next({
status: 403,
message: "You do not have permission to view this user's settings.",
});
}
next();
};
};
const isOwnProfileOrAdmin = (): Middleware => {
const authMiddleware: Middleware = (req, res, next) => {
if (
@@ -119,10 +102,28 @@ userSettingsRoutes.post<
}
const oldEmail = user.email;
const oldUsername = user.username;
user.username = req.body.username;
if (user.userType !== UserType.PLEX) {
if (user.jellyfinUsername) {
user.email = req.body.email || user.jellyfinUsername || user.email;
}
// Edge case for local users, because they have no Jellyfin username to fall back on
// if the email is not provided
if (user.userType === UserType.LOCAL) {
if (req.body.email) {
user.email = req.body.email;
if (
!user.username &&
user.email !== oldEmail &&
!oldEmail.includes('@')
) {
user.username = oldEmail;
}
} else if (req.body.username) {
user.email = oldUsername || user.email;
user.username = req.body.username;
}
}
const existingUser = await userRepository.findOne({
where: { email: user.email },
@@ -182,8 +183,9 @@ userSettingsRoutes.post<
status: e.statusCode,
message: e.errorCode,
});
} else {
return next({ status: 500, message: e.message });
}
return next({ status: 500, message: e.message });
}
});
@@ -288,260 +290,6 @@ userSettingsRoutes.post<
}
});
userSettingsRoutes.post<{ authToken: string }>(
'/linked-accounts/plex',
isOwnProfile(),
async (req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);
if (!req.user) {
return res.status(404).json({ code: ApiErrorCode.Unauthorized });
}
// Make sure Plex login is enabled
if (settings.main.mediaServerType !== MediaServerType.PLEX) {
return res.status(500).json({ message: 'Plex login is disabled' });
}
// First we need to use this auth token to get the user's email from plex.tv
const plextv = new PlexTvAPI(req.body.authToken);
const account = await plextv.getUser();
// Do not allow linking of an already linked account
if (await userRepository.exist({ where: { plexId: account.id } })) {
return res.status(422).json({
message: 'This Plex account is already linked to a Jellyseerr user',
});
}
const user = req.user;
// Emails do not match
if (user.email !== account.email) {
return res.status(422).json({
message:
'This Plex account is registered under a different email address.',
});
}
// valid plex user found, link to current user
user.userType = UserType.PLEX;
user.plexId = account.id;
user.plexUsername = account.username;
user.plexToken = account.authToken;
await userRepository.save(user);
return res.status(204).send();
}
);
userSettingsRoutes.delete<{ id: string }>(
'/linked-accounts/plex',
isOwnProfileOrAdmin(),
async (req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);
// Make sure Plex login is enabled
if (settings.main.mediaServerType !== MediaServerType.PLEX) {
return res.status(500).json({ message: 'Plex login is disabled' });
}
try {
const user = await userRepository
.createQueryBuilder('user')
.addSelect('user.password')
.where({
id: Number(req.params.id),
})
.getOne();
if (!user) {
return res.status(404).json({ message: 'User not found.' });
}
if (user.id === 1) {
return res.status(400).json({
message:
'Cannot unlink media server accounts for the primary administrator.',
});
}
if (!user.email || !user.password) {
return res.status(400).json({
message: 'User does not have a local email or password set.',
});
}
user.userType = UserType.LOCAL;
user.plexId = null;
user.plexUsername = null;
user.plexToken = null;
await userRepository.save(user);
return res.status(204).send();
} catch (e) {
return res.status(500).json({ message: e.message });
}
}
);
userSettingsRoutes.post<{ username: string; password: string }>(
'/linked-accounts/jellyfin',
isOwnProfile(),
async (req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);
if (!req.user) {
return res.status(401).json({ code: ApiErrorCode.Unauthorized });
}
// Make sure jellyfin login is enabled
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY
) {
return res
.status(500)
.json({ message: 'Jellyfin/Emby login is disabled' });
}
// Do not allow linking of an already linked account
if (
await userRepository.exist({
where: { jellyfinUsername: req.body.username },
})
) {
return res.status(422).json({
message: 'The specified account is already linked to a Jellyseerr user',
});
}
const hostname = getHostname();
const deviceId = Buffer.from(
`BOT_jellyseerr_${req.user.username ?? ''}`
).toString('base64');
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);
const ip = req.ip;
let clientIp: string | undefined;
if (ip) {
if (net.isIPv4(ip)) {
clientIp = ip;
} else if (net.isIPv6(ip)) {
clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
}
}
try {
const account = await jellyfinserver.login(
req.body.username,
req.body.password,
clientIp
);
// Do not allow linking of an already linked account
if (
await userRepository.exist({
where: { jellyfinUserId: account.User.Id },
})
) {
return res.status(422).json({
message:
'The specified account is already linked to a Jellyseerr user',
});
}
const user = req.user;
// valid jellyfin user found, link to current user
user.userType =
settings.main.mediaServerType === MediaServerType.EMBY
? UserType.EMBY
: UserType.JELLYFIN;
user.jellyfinUserId = account.User.Id;
user.jellyfinUsername = account.User.Name;
user.jellyfinAuthToken = account.AccessToken;
user.jellyfinDeviceId = deviceId;
await userRepository.save(user);
return res.status(204).send();
} catch (e) {
logger.error('Failed to link account to user.', {
label: 'API',
ip: req.ip,
error: e,
});
if (
e instanceof ApiError &&
e.errorCode === ApiErrorCode.InvalidCredentials
) {
return res.status(401).json({ code: e.errorCode });
}
return res.status(500).send();
}
}
);
userSettingsRoutes.delete<{ id: string }>(
'/linked-accounts/jellyfin',
isOwnProfileOrAdmin(),
async (req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);
// Make sure jellyfin login is enabled
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY
) {
return res
.status(500)
.json({ message: 'Jellyfin/Emby login is disabled' });
}
try {
const user = await userRepository
.createQueryBuilder('user')
.addSelect('user.password')
.where({
id: Number(req.params.id),
})
.getOne();
if (!user) {
return res.status(404).json({ message: 'User not found.' });
}
if (user.id === 1) {
return res.status(400).json({
message:
'Cannot unlink media server accounts for the primary administrator.',
});
}
if (!user.email || !user.password) {
return res.status(400).json({
message: 'User does not have a local email or password set.',
});
}
user.userType = UserType.LOCAL;
user.jellyfinUserId = null;
user.jellyfinUsername = null;
user.jellyfinAuthToken = null;
user.jellyfinDeviceId = null;
await userRepository.save(user);
return res.status(204).send();
} catch (e) {
return res.status(500).json({ message: e.message });
}
}
);
userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
'/notifications',
isOwnProfileOrAdmin(),

View File

@@ -1,4 +0,0 @@
declare module '@dr.pogodin/csurf' {
import csrf = require('csurf');
export = csrf;
}

View File

@@ -6,7 +6,7 @@ import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
export default async function createCustomProxyAgent(
proxySettings: ProxySettings
) {
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
const defaultAgent = new Agent();
const skipUrl = (url: string) => {
const hostname = new URL(url).hostname;
@@ -63,7 +63,6 @@ export default async function createCustomProxyAgent(
interceptors: {
Client: [noProxyInterceptor],
},
keepAliveTimeout: 5000,
});
setGlobalDispatcher(proxyAgent);

View File

@@ -1,24 +1,20 @@
import type { AllSettings, NetworkSettings } from '@server/lib/settings';
import type { MainSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
class RestartFlag {
private networkSettings: NetworkSettings;
private settings: MainSettings;
public initializeSettings(settings: AllSettings): void {
this.networkSettings = {
...settings.network,
proxy: { ...settings.network.proxy },
};
public initializeSettings(settings: MainSettings): void {
this.settings = { ...settings };
}
public isSet(): boolean {
const networkSettings = getSettings().network;
const settings = getSettings().main;
return (
this.networkSettings.csrfProtection !== networkSettings.csrfProtection ||
this.networkSettings.trustProxy !== networkSettings.trustProxy ||
this.networkSettings.proxy.enabled !== networkSettings.proxy.enabled ||
this.networkSettings.forceIpv4First !== networkSettings.forceIpv4First
this.settings.csrfProtection !== settings.csrfProtection ||
this.settings.trustProxy !== settings.trustProxy ||
this.settings.proxy.enabled !== settings.proxy.enabled
);
}
}

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- ***** BEGIN LICENSE BLOCK *****
- Part of the Jellyfin project (https://jellyfin.media)
-
- All copyright belongs to the Jellyfin contributors; a full list can
- be found in the file CONTRIBUTORS.md
-
- This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/.
- ***** END LICENSE BLOCK ***** -->
<svg version="1.1" id="icon-transparent" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512">
<defs>
<linearGradient id="linear-gradient" gradientUnits="userSpaceOnUse" x1="110.25" y1="213.3" x2="496.14" y2="436.09">
<stop offset="0" style="stop-color:#AA5CC3"/>
<stop offset="1" style="stop-color:#00A4DC"/>
</linearGradient>
</defs>
<title>icon-transparent</title>
<g id="icon-transparent">
<path id="inner-shape" d="M256,201.6c-20.4,0-86.2,119.3-76.2,139.4s142.5,19.9,152.4,0S276.5,201.6,256,201.6z" fill="url(#linear-gradient)"/>
<path id="outer-shape" d="M256,23.3c-61.6,0-259.8,359.4-229.6,420.1s429.3,60,459.2,0S317.6,23.3,256,23.3z
M406.5,390.8c-19.6,39.3-281.1,39.8-300.9,0s110.1-275.3,150.4-275.3S426.1,351.4,406.5,390.8z" fill="url(#linear-gradient)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,43 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 361 157">
<defs>
<style>
.cls-1 {
fill: #fff;
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
.cls-2 {
fill: #eaaf20;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Layer_1">
<path id="path4" class="cls-1"
d="M60.6,28.8c-14.3,0-23.5,3.9-31.3,13v-10H1.6v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM55.2,104.3c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z" />
<path id="path6" class="cls-1"
d="M148.1,76.5c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.9h28.4l.2,75.6Z" />
<polygon id="polygon8" class="cls-2"
points="287.6 78.3 254.1 31.7 288.6 31.7 321.8 78.3 288.6 124.6 254.1 124.6 287.6 78.3" />
<polygon id="polygon10" class="cls-1"
points="330.8 73 360.6 31.7 326.2 31.7 313.8 48.9 330.8 73" />
<path id="path12" class="cls-1"
d="M313.8,107.7l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z" />
<path id="path16" class="cls-1"
d="M228.7,97.9c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM211.9,50.7c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z" />
<path id="path4-2" data-name="path4" class="cls-1"
d="M59.3,28.2c-14.3,0-23.5,3.9-31.3,13v-10H.4v123.7s.5.2,1.9.5c1.9.5,12.1,2.5,19.6-3.4,6.5-5.3,8-11.4,8-18.3v-17.8c8,8,17,11.4,29.6,11.4,27.2,0,48-20.8,48-48.4s-20.1-50.7-48.3-50.7h0ZM54,103.8c-15.3,0-27.4-11.9-27.4-26.3s14.3-25.6,27.4-25.6,27.4,11.2,27.4,25.8-12.1,26-27.4,26Z" />
<path id="path6-2" data-name="path6" class="cls-1"
d="M146.9,75.9c0,10.7,1.2,23.7,12.4,37.9.2.2.7.9.7.9-4.6,7.3-10.2,12.3-17.7,12.3s-11.6-3-16.5-8c-5.1-5.5-7.5-12.6-7.5-20.1V.4h28.4l.2,75.6Z" />
<polygon id="polygon8-2" data-name="polygon8" class="cls-2"
points="286.4 77.8 252.9 31.2 287.3 31.2 320.6 77.8 287.3 124.1 252.9 124.1 286.4 77.8" />
<polygon id="polygon10-2" data-name="polygon10" class="cls-1"
points="329.5 72.5 359.4 31.2 324.9 31.2 312.6 48.3 329.5 72.5" />
<path id="path12-2" data-name="path12" class="cls-1"
d="M312.6,107.2l5.8,7.5c5.6,8.2,12.9,12.3,21.3,12.3,9-.2,15.3-7.5,17.7-10.3,0,0-4.4-3.7-9.9-9.8-7.5-8.2-17.5-23.3-17.7-24l-17.2,24.2Z" />
<path id="path16-2" data-name="path16" class="cls-1"
d="M227.4,97.4c-5.8,5-9.7,7.8-17.7,7.8-14.3,0-22.6-9.6-23.8-20.1h75.9c.5-1.4.7-3.2.7-6.2,0-29-22.6-50.7-52.2-50.7s-51.2,22.1-51.2,49.8,23,49.1,51.9,49.1,37.6-10.7,47.1-29.7h-30.8ZM210.7,50.1c12.6,0,22.1,7.8,24.3,18h-48c2.4-10.7,11.4-18,23.8-18h0Z" />
</g>
</g>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="plex-logo"
x="0px"
y="0px"
viewBox="0 0 1000 460.89727"
xml:space="preserve"
sodipodi:docname="plex-logo.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata25"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs23">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
id="namedview21"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="0.27956081"
inkscape:cx="783.06912"
inkscape:cy="-132.85701"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="plex-logo" />
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
.st1{fill:#EBAF00;}
</style>
<path
class="st0"
d="m 164.18919,82.43243 c -39.86487,0 -65.540543,11.48648 -87.162163,38.51351 V 91.21621 H 0 v 366.21621 c 0,0 1.3513514,0.67567 5.4054053,1.35135 5.4054057,1.35135 33.7837827,7.43243 54.7297287,-10.13514 18.243244,-15.54054 22.297295,-33.78378 22.297295,-54.05405 v -52.7027 c 22.297301,23.64864 47.297301,33.78378 82.432431,33.78378 75.67567,0 133.78378,-61.48648 133.78378,-143.24323 0,-88.51352 -56.08108,-150 -134.45945,-150 z m -14.86487,223.64864 c -42.56756,0 -76.351351,-35.13513 -76.351351,-77.7027 0,-41.89189 39.864871,-75.67567 76.351351,-75.67567 43.24324,0 76.35135,33.1081 76.35135,76.35135 0,43.24324 -33.78378,77.02702 -76.35135,77.02702 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><path
class="st0"
d="m 408.1081,223.64864 c 0,31.75676 3.37838,70.27027 34.45946,112.16216 0.67567,0.67567 2.02702,2.7027 2.02702,2.7027 -12.83783,21.62162 -28.37837,36.48648 -49.32432,36.48648 -16.21622,0 -32.43243,-8.78378 -45.94595,-23.64864 -14.18918,-16.21622 -20.94594,-37.16216 -20.94594,-59.45946 V 0 h 79.05405 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><polygon
class="st1"
points="117.9,33.9 104.1,13.5 118.3,13.5 132,33.9 118.3,54.2 104.1,54.2 "
id="polygon8"
style="fill:#ebaf00"
transform="scale(6.7567568)" /><polygon
class="st0"
points="135.7,31.6 148,13.5 133.8,13.5 128.7,21 "
id="polygon10"
style="fill:#ffffff"
transform="scale(6.7567568)" /><path
class="st0"
d="m 869.59458,316.2162 c 0,0 16.2162,22.2973 16.2162,22.2973 15.54058,24.32432 35.8108,36.48648 59.45949,36.48648 25,-0.67567 42.56752,-22.29729 49.3243,-30.4054 0,0 -12.16218,-10.81081 -27.7027,-29.05405 -20.94598,-24.32432 -48.64868,-68.91892 -49.3243,-70.94594 z"
id="path12"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:6.75675678" /><path
style="fill:#ffffff;stroke-width:6.75675678"
inkscape:connector-curvature="0"
id="path16"
d="m 632.43242,287.16215 c -16.21622,14.86486 -27.02703,22.97297 -49.32432,22.97297 -39.86487,0 -62.83784,-28.37837 -66.21622,-59.45945 h 211.4865 c 1.35131,-4.05406 2.027,-9.45946 2.027,-18.24324 0,-85.81082 -62.83783,-150 -145.27026,-150 -78.37837,0 -142.56756,65.54054 -142.56756,147.29729 0,81.08108 64.18919,145.27026 144.59459,145.27026 56.08108,0 104.72973,-31.75675 131.08105,-87.83783 z M 585.8108,147.29729 c 35.13513,0 61.48648,22.97297 67.56756,53.37838 H 519.59458 c 6.75676,-31.75676 31.75676,-53.37838 66.21622,-53.37838 z"
class="st0" />
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -14,7 +14,6 @@ type AirDateBadgeProps = {
const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
const WEEK = 1000 * 60 * 60 * 24 * 8;
const intl = useIntl();
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const dAirDate = new Date(airDate);
const nowDate = new Date();
const alreadyAired = dAirDate.getTime() < nowDate.getTime();
@@ -39,7 +38,7 @@ const AirDateBadge = ({ airDate }: AirDateBadgeProps) => {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone,
timeZone: 'UTC',
})}
</Badge>
{showRelative && (

View File

@@ -298,7 +298,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
src={
title?.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/jellyseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -233,7 +233,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/jellyseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -1,6 +1,5 @@
import type { ForwardedRef } from 'react';
import React from 'react';
import { twMerge } from 'tailwind-merge';
export type ButtonType =
| 'default'
@@ -98,7 +97,7 @@ function Button<P extends ElementTypes = 'button'>(
if (as === 'a') {
return (
<a
className={twMerge(buttonStyle)}
className={buttonStyle.join(' ')}
{...(props as React.ComponentProps<'a'>)}
ref={ref as ForwardedRef<HTMLAnchorElement>}
>
@@ -108,7 +107,7 @@ function Button<P extends ElementTypes = 'button'>(
} else {
return (
<button
className={twMerge(buttonStyle)}
className={buttonStyle.join(' ')}
{...(props as React.ComponentProps<'button'>)}
ref={ref as ForwardedRef<HTMLButtonElement>}
>

View File

@@ -1,29 +1,77 @@
import Dropdown from '@app/components/Common/Dropdown';
import useClickOutside from '@app/hooks/useClickOutside';
import { withProperties } from '@app/utils/typeHelpers';
import { Menu } from '@headlessui/react';
import { Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import type {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
RefObject,
} from 'react';
import { Fragment, useRef, useState } from 'react';
type ButtonWithDropdownProps = {
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost';
}
const DropdownItem = ({
children,
buttonType = 'primary',
...props
}: DropdownItemProps) => {
let styleClass = 'button-md text-white';
switch (buttonType) {
case 'ghost':
styleClass +=
' bg-transparent rounded hover:bg-gradient-to-br from-indigo-600 to-purple-600 text-white focus:border-gray-500 focus:text-white';
break;
default:
styleClass +=
' bg-indigo-600 rounded hover:bg-indigo-500 focus:border-indigo-700 focus:text-white';
}
return (
<a
className={`flex cursor-pointer items-center px-4 py-2 text-sm leading-5 focus:outline-none ${styleClass}`}
{...props}
>
{children}
</a>
);
};
interface ButtonWithDropdownProps {
text: React.ReactNode;
dropdownIcon?: React.ReactNode;
buttonType?: 'primary' | 'ghost';
} & (
| ({ as?: 'button' } & ButtonHTMLAttributes<HTMLButtonElement>)
| ({ as: 'a' } & AnchorHTMLAttributes<HTMLAnchorElement>)
);
}
interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
ButtonWithDropdownProps {
as?: 'button';
}
interface AnchorProps
extends AnchorHTMLAttributes<HTMLAnchorElement>,
ButtonWithDropdownProps {
as: 'a';
}
const ButtonWithDropdown = ({
as,
text,
children,
dropdownIcon,
className,
buttonType = 'primary',
...props
}: ButtonWithDropdownProps) => {
}: ButtonProps | AnchorProps) => {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
useClickOutside(buttonRef, () => setIsOpen(false));
const styleClasses = {
mainButtonClasses: 'button-md text-white border',
dropdownSideButtonClasses: 'button-md border',
dropdownClasses: 'button-md',
};
switch (buttonType) {
@@ -31,40 +79,72 @@ const ButtonWithDropdown = ({
styleClasses.mainButtonClasses +=
' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses;
styleClasses.dropdownClasses +=
' bg-gray-800 border border-gray-700 bg-opacity-80 p-1 backdrop-blur';
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';
styleClasses.dropdownSideButtonClasses +=
' bg-indigo-600 bg-opacity-80 border-indigo-500 hover:bg-opacity-100 active:bg-opacity-100 focus:ring-blue';
styleClasses.dropdownClasses += ' bg-indigo-600 p-1';
}
const TriggerElement = props.as ?? 'button';
return (
<Menu as="div" className="relative z-10 inline-flex">
<TriggerElement
type="button"
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
styleClasses.mainButtonClasses
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
{...(props as Record<string, string>)}
>
{text}
</TriggerElement>
<span className="relative inline-flex h-full rounded-md shadow-sm">
{as === 'a' ? (
<a
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
styleClasses.mainButtonClasses
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
ref={buttonRef as RefObject<HTMLAnchorElement>}
{...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}
>
{text}
</a>
) : (
<button
type="button"
className={`relative z-10 inline-flex h-full items-center px-4 py-2 text-sm font-medium leading-5 transition duration-150 ease-in-out hover:z-20 focus:z-20 focus:outline-none ${
styleClasses.mainButtonClasses
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
ref={buttonRef as RefObject<HTMLButtonElement>}
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}
>
{text}
</button>
)}
{children && (
<span className="relative -ml-px block">
<Menu.Button
<button
type="button"
className={`relative z-10 inline-flex h-full items-center rounded-r-md px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out hover:z-20 focus:z-20 ${styleClasses.dropdownSideButtonClasses}`}
aria-label="Expand"
onClick={() => setIsOpen((state) => !state)}
>
{dropdownIcon ? dropdownIcon : <ChevronDownIcon />}
</Menu.Button>
<Dropdown.Items dropdownType={buttonType}>{children}</Dropdown.Items>
</button>
<Transition
as={Fragment}
show={isOpen}
enter="transition ease-out duration-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="absolute right-0 z-40 mt-2 -mr-1 w-56 origin-top-right rounded-md shadow-lg">
<div
className={`rounded-md ring-1 ring-black ring-opacity-5 ${styleClasses.dropdownClasses}`}
>
<div className="py-1">{children}</div>
</div>
</div>
</Transition>
</span>
)}
</Menu>
</span>
);
};
export default withProperties(ButtonWithDropdown, { Item: Dropdown.Item });
export default withProperties(ButtonWithDropdown, { Item: DropdownItem });

View File

@@ -1,117 +0,0 @@
import { withProperties } from '@app/utils/typeHelpers';
import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/solid';
import {
Fragment,
useRef,
type AnchorHTMLAttributes,
type ButtonHTMLAttributes,
type HTMLAttributes,
} from 'react';
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost';
}
const DropdownItem = ({
children,
buttonType = 'primary',
...props
}: DropdownItemProps) => {
return (
<Menu.Item>
<a
className={[
'button-md flex cursor-pointer items-center rounded px-4 py-2 text-sm leading-5 text-white focus:text-white focus:outline-none',
buttonType === 'ghost'
? 'bg-transparent from-indigo-600 to-purple-600 hover:bg-gradient-to-br focus:border-gray-500'
: 'bg-indigo-600 hover:bg-indigo-500 focus:border-indigo-700',
].join(' ')}
{...props}
>
{children}
</a>
</Menu.Item>
);
};
type DropdownItemsProps = HTMLAttributes<HTMLDivElement> & {
dropdownType: 'primary' | 'ghost';
};
const DropdownItems = ({
children,
className,
dropdownType,
...props
}: DropdownItemsProps) => {
return (
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Menu.Items
className={[
'absolute right-0 z-40 mt-2 -mr-1 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'
: 'bg-indigo-600',
className,
].join(' ')}
{...props}
>
<div className="py-1">{children}</div>
</Menu.Items>
</Transition>
);
};
interface DropdownProps extends ButtonHTMLAttributes<HTMLButtonElement> {
text: React.ReactNode;
dropdownIcon?: React.ReactNode;
buttonType?: 'primary' | 'ghost';
}
const Dropdown = ({
text,
children,
dropdownIcon,
className,
buttonType = 'primary',
...props
}: DropdownProps) => {
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<Menu as="div" className="relative z-10">
<Menu.Button
type="button"
className={[
'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',
className,
].join(' ')}
ref={buttonRef}
disabled={!children}
{...props}
>
<span>{text}</span>
{children && (dropdownIcon ? dropdownIcon : <ChevronDownIcon />)}
</Menu.Button>
{children && (
<DropdownItems dropdownType={buttonType}>{children}</DropdownItems>
)}
</Menu>
);
};
export default withProperties(Dropdown, {
Item: DropdownItem,
Items: DropdownItems,
});

View File

@@ -1,44 +0,0 @@
import { Field } from 'formik';
import { twMerge } from 'tailwind-merge';
interface LabeledCheckboxProps {
id: string;
className?: string;
label: string;
description: string;
onChange: () => void;
children?: React.ReactNode;
}
const LabeledCheckbox: React.FC<LabeledCheckboxProps> = ({
id,
className,
label,
description,
onChange,
children,
}) => {
return (
<>
<div className={twMerge('relative flex items-start', className)}>
<div className="flex h-6 items-center">
<Field type="checkbox" id={id} name={id} onChange={onChange} />
</div>
<div className="ml-3 text-sm leading-6">
<label htmlFor="localLogin" className="block">
<div className="flex flex-col">
<span className="font-medium text-white">{label}</span>
<span className="font-normal text-gray-400">{description}</span>
</div>
</label>
</div>
</div>
{
/* can hold child checkboxes */
children && <div className="mt-4 pl-10">{children}</div>
}
</>
);
};
export default LabeledCheckbox;

View File

@@ -29,16 +29,11 @@ interface ModalProps {
secondaryDisabled?: boolean;
tertiaryDisabled?: boolean;
tertiaryButtonType?: ButtonType;
okButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
cancelButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
secondaryButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
tertiaryButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
disableScrollLock?: boolean;
backgroundClickable?: boolean;
loading?: boolean;
backdrop?: string;
children?: React.ReactNode;
dialogClass?: string;
}
const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
@@ -66,11 +61,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
loading = false,
onTertiary,
backdrop,
dialogClass,
okButtonProps,
cancelButtonProps,
secondaryButtonProps,
tertiaryButtonProps,
},
parentRef
) => {
@@ -116,7 +106,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
</div>
</Transition>
<Transition
className={`hide-scrollbar relative inline-block w-full overflow-auto bg-gray-800 px-4 pt-4 pb-4 text-left align-bottom shadow-xl ring-1 ring-gray-700 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle ${dialogClass}`}
className="hide-scrollbar relative inline-block w-full overflow-auto bg-gray-800 px-4 pt-4 pb-4 text-left align-bottom shadow-xl ring-1 ring-gray-700 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
@@ -199,7 +189,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
className="ml-3"
disabled={okDisabled}
data-testid="modal-ok-button"
{...okButtonProps}
>
{okText ? okText : 'Ok'}
</Button>
@@ -211,7 +200,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
className="ml-3"
disabled={secondaryDisabled}
data-testid="modal-secondary-button"
{...secondaryButtonProps}
>
{secondaryText}
</Button>
@@ -222,7 +210,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
{...tertiaryButtonProps}
>
{tertiaryText}
</Button>
@@ -233,7 +220,6 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
onClick={onCancel}
className="ml-3 sm:ml-0"
data-testid="modal-cancel-button"
{...cancelButtonProps}
>
{cancelText
? cancelText

View File

@@ -25,11 +25,6 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => {
return (
<>
<Component
autoComplete="off"
data-form-type="other"
data-1pignore="true"
data-lpignore="true"
data-bwignore="true"
{...componentProps}
className={`rounded-l-only ${componentProps.className ?? ''}`}
type={

View File

@@ -33,7 +33,7 @@ import { useRouter } from 'next/router';
import { useState } from 'react';
import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import * as Yup from 'yup';
const messages = defineMessages('components.IssueDetails', {
@@ -155,7 +155,6 @@ const IssueDetails = () => {
autoDismiss: true,
});
revalidateIssue();
mutate('/api/v1/issue/count');
} catch (e) {
addToast(intl.formatMessage(messages.toaststatusupdatefailed), {
appearance: 'error',
@@ -170,7 +169,6 @@ const IssueDetails = () => {
method: 'DELETE',
});
if (!res.ok) throw new Error();
mutate('/api/v1/issue/count');
addToast(intl.formatMessage(messages.toastissuedeleted), {
appearance: 'success',
@@ -242,7 +240,7 @@ const IssueDetails = () => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/jellyseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -142,7 +142,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
src={
title.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
: '/images/jellyseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"

View File

@@ -15,7 +15,7 @@ import { Field, Formik } from 'formik';
import Link from 'next/link';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
import * as Yup from 'yup';
const messages = defineMessages('components.IssueModal.CreateIssueModal', {
@@ -138,8 +138,6 @@ const CreateIssueModal = ({
autoDismiss: true,
}
);
mutate('/api/v1/issue/count');
}
if (onCancel) {

View File

@@ -33,7 +33,6 @@ interface LanguageSelectorProps {
setFieldValue: (property: string, value: string) => void;
serverValue?: string;
isUserSettings?: boolean;
isDisabled?: boolean;
}
const LanguageSelector = ({
@@ -41,7 +40,6 @@ const LanguageSelector = ({
setFieldValue,
serverValue,
isUserSettings = false,
isDisabled,
}: LanguageSelectorProps) => {
const intl = useIntl();
const { data: languages } = useSWR<Language[]>('/api/v1/languages');
@@ -98,7 +96,6 @@ const LanguageSelector = ({
<Select<OptionType, true>
options={options}
isMulti
isDisabled={isDisabled}
className="react-select-container"
classNamePrefix="react-select"
value={

View File

@@ -1,4 +1,3 @@
import Badge from '@app/components/Common/Badge';
import { menuMessages } from '@app/components/Layout/Sidebar';
import useClickOutside from '@app/hooks/useClickOutside';
import { Permission, useUser } from '@app/hooks/useUser';
@@ -27,16 +26,9 @@ import {
} from '@heroicons/react/24/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { cloneElement, useEffect, useRef, useState } from 'react';
import { cloneElement, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
interface MobileMenuProps {
pendingRequestsCount: number;
openIssuesCount: number;
revalidateIssueCount: () => void;
revalidateRequestsCount: () => void;
}
interface MenuLink {
href: string;
svgIcon: JSX.Element;
@@ -49,12 +41,7 @@ interface MenuLink {
dataTestId?: string;
}
const MobileMenu = ({
pendingRequestsCount,
openIssuesCount,
revalidateIssueCount,
revalidateRequestsCount,
}: MobileMenuProps) => {
const MobileMenu = () => {
const ref = useRef<HTMLDivElement>(null);
const intl = useIntl();
const [isOpen, setIsOpen] = useState(false);
@@ -152,21 +139,6 @@ const MobileMenu = ({
})
);
useEffect(() => {
if (openIssuesCount) {
revalidateIssueCount();
}
if (pendingRequestsCount) {
revalidateRequestsCount();
}
}, [
revalidateIssueCount,
revalidateRequestsCount,
pendingRequestsCount,
openIssuesCount,
]);
return (
<div className="fixed bottom-0 left-0 right-0 z-50">
<Transition
@@ -187,7 +159,7 @@ const MobileMenu = ({
<Link
key={`mobile-menu-link-${link.href}`}
href={link.href}
className={`flex items-center ${
className={`flex items-center space-x-2 ${
isActive ? 'text-indigo-500' : ''
}`}
onKeyDown={(e) => {
@@ -202,25 +174,7 @@ const MobileMenu = ({
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
className: 'h-5 w-5',
})}
<span className="ml-2">{link.content}</span>
{link.href === '/requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="ml-auto flex">
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
{pendingRequestsCount}
</Badge>
</div>
)}
{link.href === '/issues' &&
openIssuesCount > 0 &&
hasPermission(Permission.MANAGE_ISSUES) && (
<div className="ml-auto flex">
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
{openIssuesCount}
</Badge>
</div>
)}
<span>{link.content}</span>
</Link>
);
})}
@@ -236,7 +190,7 @@ const MobileMenu = ({
<Link
key={`mobile-menu-link-${link.href}`}
href={link.href}
className={`relative flex flex-col items-center space-y-1 ${
className={`flex flex-col items-center space-y-1 ${
isActive ? 'text-indigo-500' : ''
}`}
>
@@ -246,23 +200,6 @@ const MobileMenu = ({
className: 'h-6 w-6',
}
)}
{link.href === '/requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="absolute left-3 bottom-3">
<Badge
className={`bg-gradient-to-br ${
router.pathname.match(link.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
} flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`}
>
{pendingRequestsCount > 99
? '99+'
: pendingRequestsCount}
</Badge>
</div>
)}
</Link>
);
})}

View File

@@ -1,4 +1,3 @@
import Badge from '@app/components/Common/Badge';
import UserWarnings from '@app/components/Layout/UserWarnings';
import VersionStatus from '@app/components/Layout/VersionStatus';
import useClickOutside from '@app/hooks/useClickOutside';
@@ -19,7 +18,7 @@ import {
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useEffect, useRef } from 'react';
import { Fragment, useRef } from 'react';
import { useIntl } from 'react-intl';
export const menuMessages = defineMessages('components.Layout.Sidebar', {
@@ -36,10 +35,6 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
interface SidebarProps {
open?: boolean;
setClosed: () => void;
pendingRequestsCount: number;
openIssuesCount: number;
revalidateIssueCount: () => void;
revalidateRequestsCount: () => void;
}
interface SidebarLinkProps {
@@ -119,35 +114,13 @@ const SidebarLinks: SidebarLinkProps[] = [
},
];
const Sidebar = ({
open,
setClosed,
pendingRequestsCount,
openIssuesCount,
revalidateIssueCount,
revalidateRequestsCount,
}: SidebarProps) => {
const Sidebar = ({ open, setClosed }: SidebarProps) => {
const navRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const intl = useIntl();
const { hasPermission } = useUser();
useClickOutside(navRef, () => setClosed());
useEffect(() => {
if (openIssuesCount) {
revalidateIssueCount();
}
if (pendingRequestsCount) {
revalidateRequestsCount();
}
}, [
revalidateIssueCount,
revalidateRequestsCount,
pendingRequestsCount,
openIssuesCount,
]);
return (
<>
<div className="lg:hidden">
@@ -280,48 +253,18 @@ const Sidebar = ({
href={sidebarLink.href}
as={sidebarLink.as}
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={sidebarLink.dataTestId}
>
{sidebarLink.svgIcon}
{intl.formatMessage(
menuMessages[sidebarLink.messagesKey]
)}
{sidebarLink.messagesKey === 'requests' &&
pendingRequestsCount > 0 &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<div className="ml-auto flex">
<Badge
className={`rounded-md bg-gradient-to-br ${
router.pathname.match(sidebarLink.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
}`}
>
{pendingRequestsCount}
</Badge>
</div>
)}
{sidebarLink.messagesKey === 'issues' &&
openIssuesCount > 0 &&
hasPermission(Permission.MANAGE_ISSUES) && (
<div className="ml-auto flex">
<Badge
className={`rounded-md bg-gradient-to-br ${
router.pathname.match(sidebarLink.activeRegExp)
? 'border-indigo-600 from-indigo-700 to-purple-700'
: 'border-indigo-500 from-indigo-600 to-purple-600'
}`}
>
{openIssuesCount}
</Badge>
</div>
)}
</Link>
);
})}

View File

@@ -10,7 +10,6 @@ import { useUser } from '@app/hooks/useUser';
import { ArrowLeftIcon, Bars3BottomLeftIcon } from '@heroicons/react/24/solid';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
type LayoutProps = {
children: React.ReactNode;
@@ -23,18 +22,6 @@ const Layout = ({ children }: LayoutProps) => {
const router = useRouter();
const { currentSettings } = useSettings();
const { setLocale } = useLocale();
const { data: requestResponse, mutate: revalidateRequestsCount } = useSWR(
'/api/v1/request/count',
{
revalidateOnMount: true,
}
);
const { data: issueResponse, mutate: revalidateIssueCount } = useSWR(
'/api/v1/issue/count',
{
revalidateOnMount: true,
}
);
useEffect(() => {
if (setLocale && user) {
@@ -68,21 +55,10 @@ const Layout = ({ children }: LayoutProps) => {
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
<div className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
</div>
<Sidebar
open={isSidebarOpen}
setClosed={() => setSidebarOpen(false)}
pendingRequestsCount={requestResponse?.pending ?? 0}
openIssuesCount={issueResponse?.open ?? 0}
revalidateIssueCount={() => revalidateIssueCount()}
revalidateRequestsCount={() => revalidateRequestsCount()}
/>
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
<div className="sm:hidden">
<MobileMenu
pendingRequestsCount={requestResponse?.pending ?? 0}
openIssuesCount={issueResponse?.open ?? 0}
revalidateIssueCount={() => revalidateIssueCount()}
revalidateRequestsCount={() => revalidateRequestsCount()}
/>
<MobileMenu />
</div>
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">

View File

@@ -1,39 +1,63 @@
import Button from '@app/components/Common/Button';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import Tooltip from '@app/components/Common/Tooltip';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error';
import { MediaServerType, ServerType } from '@server/constants/server';
import { Field, Form, Formik } from 'formik';
import { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup';
const messages = defineMessages('components.Login', {
loginwithapp: 'Login with {appName}',
username: 'Username',
password: 'Password',
hostname: '{mediaServerName} URL',
port: 'Port',
enablessl: 'Use SSL',
urlBase: 'URL Base',
email: 'Email',
emailtooltip:
'Address does not need to be associated with your {mediaServerName} instance.',
validationhostrequired: '{mediaServerName} URL required',
validationhostformat: 'Valid URL required',
validationemailrequired: 'Email required',
validationemailformat: 'Valid email required',
validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required',
validationservertyperequired: 'Please select a server type',
validationHostnameRequired: 'You must provide a valid hostname or IP address',
validationPortRequired: 'You must provide a valid port number',
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
loginerror: 'Something went wrong while trying to sign in.',
adminerror: 'You must use an admin account to sign in.',
noadminerror: 'No admin user found on the server.',
credentialerror: 'The username or password is incorrect.',
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
signingin: 'Signing In…',
signingin: 'Signing in…',
signin: 'Sign In',
initialsigningin: 'Connecting…',
initialsignin: 'Connect',
forgotpassword: 'Forgot Password?',
servertype: 'Server Type',
back: 'Go back',
});
interface JellyfinLoginProps {
revalidate: () => void;
initial?: boolean;
serverType?: MediaServerType;
onCancel?: () => void;
}
const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
revalidate,
initial,
serverType,
onCancel,
}) => {
const toasts = useToasts();
const intl = useIntl();
@@ -48,29 +72,56 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
: 'Media Server',
};
const LoginSchema = Yup.object().shape({
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string(),
});
const baseUrl = settings.currentSettings.jellyfinExternalHost
? settings.currentSettings.jellyfinExternalHost
: settings.currentSettings.jellyfinHost;
const jellyfinForgotPasswordUrl =
settings.currentSettings.jellyfinForgotPasswordUrl;
if (initial) {
const LoginSchema = Yup.object().shape({
hostname: Yup.string().required(
intl.formatMessage(
messages.validationhostrequired,
mediaServerFormatValues
)
),
port: Yup.number().required(
intl.formatMessage(messages.validationPortRequired)
),
urlBase: Yup.string()
.test(
'leading-slash',
intl.formatMessage(messages.validationUrlBaseLeadingSlash),
(value) => !value || value.startsWith('/')
)
.test(
'trailing-slash',
intl.formatMessage(messages.validationUrlBaseTrailingSlash),
(value) => !value || !value.endsWith('/')
),
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string(),
});
return (
<div>
return (
<Formik
initialValues={{
username: '',
password: '',
hostname: '',
port: 8096,
useSsl: false,
urlBase: '',
email: '',
}}
validationSchema={LoginSchema}
validateOnBlur={false}
onSubmit={async (values) => {
try {
// Check if serverType is either 'Jellyfin' or 'Emby'
// if (serverType !== 'Jellyfin' && serverType !== 'Emby') {
// throw new Error('Invalid serverType'); // You can customize the error message
// }
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
@@ -79,7 +130,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
body: JSON.stringify({
username: values.username,
password: values.password,
email: values.username,
hostname: values.hostname,
port: values.port,
useSsl: values.useSsl,
urlBase: values.urlBase,
email: values.email,
serverType: serverType,
}),
});
if (!res.ok) throw new Error(res.statusText, { cause: res });
@@ -109,6 +165,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
errorMessage = messages.loginerror;
break;
}
toasts.addToast(
intl.formatMessage(errorMessage, mediaServerFormatValues),
{
@@ -121,56 +178,303 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
return (
<>
<Form data-form-type="login">
<div>
<h2 className="mb-6 -mt-1 text-center text-lg font-bold text-neutral-200">
{intl.formatMessage(messages.loginwithapp, {
appName: mediaServerFormatValues.mediaServerName,
})}
</h2>
<div className="mt-1 mb-4">
<div className="form-input-field">
{({
errors,
touched,
values,
setFieldValue,
isSubmitting,
isValid,
}) => (
<Form>
<div className="sm:border-t sm:border-gray-800">
<div className="flex flex-col sm:flex-row sm:gap-4">
<div className="w-full">
<label htmlFor="hostname" className="text-label">
{intl.formatMessage(
messages.hostname,
mediaServerFormatValues
)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mb-0 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
{values.useSsl ? 'https://' : 'http://'}
</span>
<Field
id="username"
name="username"
id="hostname"
name="hostname"
type="text"
placeholder={intl.formatMessage(messages.username)}
className="!bg-gray-700/80 placeholder:text-gray-400"
data-form-type="username"
className="rounded-r-only flex-1"
placeholder={intl.formatMessage(
messages.hostname,
mediaServerFormatValues
)}
/>
</div>
{errors.username && touched.username && (
<div className="error">{errors.username}</div>
{errors.hostname && touched.hostname && (
<div className="error">{errors.hostname}</div>
)}
</div>
<div className="mt-1 mb-2">
<div className="form-input-field">
<SensitiveInput
as="field"
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder={intl.formatMessage(messages.password)}
className="!bg-gray-700/80 placeholder:text-gray-400"
data-form-type="password"
data-1pignore="false"
data-lpignore="false"
data-bwignore="false"
/>
</div>
<div className="flex-1">
<label htmlFor="port" className="text-label">
{intl.formatMessage(messages.port)}
</label>
<div className="mt-1 sm:mt-0">
<Field
id="port"
name="port"
inputMode="numeric"
type="text"
className="short flex-1"
placeholder={intl.formatMessage(messages.port)}
/>
{errors.port && touched.port && (
<div className="error">{errors.port}</div>
)}
</div>
</div>
</div>
<label htmlFor="useSsl" className="text-label mt-2">
{intl.formatMessage(messages.enablessl)}
</label>
<div className="mt-1 mb-2 sm:col-span-2">
<div className="flex rounded-md shadow-sm">
<Field
id="useSsl"
name="useSsl"
type="checkbox"
onChange={() => {
setFieldValue('useSsl', !values.useSsl);
setFieldValue('port', values.useSsl ? 8096 : 443);
}}
/>
</div>
</div>
<label htmlFor="urlBase" className="text-label mt-1">
{intl.formatMessage(messages.urlBase)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<Field
type="text"
inputMode="url"
id="urlBase"
name="urlBase"
placeholder={intl.formatMessage(messages.urlBase)}
/>
</div>
{errors.urlBase && touched.urlBase && (
<div className="error">{errors.urlBase}</div>
)}
</div>
<label
htmlFor="email"
className="text-label inline-flex gap-1 align-middle"
>
{intl.formatMessage(messages.email)}
<span className="label-tip">
<Tooltip
content={intl.formatMessage(
messages.emailtooltip,
mediaServerFormatValues
)}
>
<span className="tooltip-trigger">
<InformationCircleIcon className="h-4 w-4" />
</span>
</Tooltip>
</span>
</label>
<div className="mt-1 sm:col-span-2 sm:mb-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
placeholder={intl.formatMessage(messages.email)}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
<label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
<Field
id="username"
name="username"
type="text"
placeholder={intl.formatMessage(messages.username)}
/>
</div>
{errors.username && touched.username && (
<div className="error">{errors.username}</div>
)}
</div>
<label htmlFor="password" className="text-label">
{intl.formatMessage(messages.password)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flexrounded-md shadow-sm">
<Field
id="password"
name="password"
type="password"
placeholder={intl.formatMessage(messages.password)}
/>
</div>
{errors.password && touched.password && (
<div className="error">{errors.password}</div>
)}
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex flex-row-reverse justify-between">
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</Button>
</span>
{onCancel && (
<span className="inline-flex rounded-md shadow-sm">
<Button buttonType="default" onClick={() => onCancel()}>
<FormattedMessage {...messages.back} />
</Button>
</span>
)}
</div>
</div>
</Form>
)}
</Formik>
);
} else {
const LoginSchema = Yup.object().shape({
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string(),
});
const baseUrl = settings.currentSettings.jellyfinExternalHost
? settings.currentSettings.jellyfinExternalHost
: settings.currentSettings.jellyfinHost;
const jellyfinForgotPasswordUrl =
settings.currentSettings.jellyfinForgotPasswordUrl;
return (
<div>
<Formik
initialValues={{
username: '',
password: '',
}}
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: values.username,
password: values.password,
email: values.username,
}),
});
if (!res.ok) throw new Error(res.statusText, { cause: res });
} catch (e) {
let errorData;
try {
errorData = await e.cause?.text();
errorData = JSON.parse(errorData);
} catch {
/* empty */
}
let errorMessage = null;
switch (errorData?.message) {
case ApiErrorCode.InvalidUrl:
errorMessage = messages.invalidurlerror;
break;
case ApiErrorCode.InvalidCredentials:
errorMessage = messages.credentialerror;
break;
case ApiErrorCode.NotAdmin:
errorMessage = messages.adminerror;
break;
case ApiErrorCode.NoAdminUser:
errorMessage = messages.noadminerror;
break;
default:
errorMessage = messages.loginerror;
break;
}
toasts.addToast(
intl.formatMessage(errorMessage, mediaServerFormatValues),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
return (
<>
<Form>
<div className="sm:border-t sm:border-gray-800">
<label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="username"
name="username"
type="text"
placeholder={intl.formatMessage(messages.username)}
/>
</div>
{errors.username && touched.username && (
<div className="error">{errors.username}</div>
)}
</div>
<div className="flex">
<label htmlFor="password" className="text-label">
{intl.formatMessage(messages.password)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="password"
name="password"
type="password"
placeholder={intl.formatMessage(messages.password)}
/>
</div>
{errors.password && touched.password && (
<div className="error">{errors.password}</div>
)}
<div className="flex-grow"></div>
{baseUrl && (
<a
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex justify-between">
<span className="inline-flex rounded-md shadow-sm">
<Button
as="a"
buttonType="ghost"
href={
jellyfinForgotPasswordUrl
? `${jellyfinForgotPasswordUrl}`
@@ -181,35 +485,31 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
: ''
}forgotpassword.html`
}
className="pt-2 text-sm text-indigo-500 hover:text-indigo-400"
>
{intl.formatMessage(messages.forgotpassword)}
</a>
)}
</Button>
</span>
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</Button>
</span>
</div>
</div>
</div>
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
className="mt-2 w-full shadow-sm"
>
<ArrowLeftOnRectangleIcon />
<span>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</span>
</Button>
</Form>
</>
);
}}
</Formik>
</div>
);
</Form>
</>
);
}}
</Formik>
</div>
);
}
};
export default JellyfinLogin;

View File

@@ -2,7 +2,10 @@ import Button from '@app/components/Common/Button';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import {
ArrowLeftOnRectangleIcon,
LifebuoyIcon,
} from '@heroicons/react/24/outline';
import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useState } from 'react';
@@ -10,7 +13,6 @@ import { useIntl } from 'react-intl';
import * as Yup from 'yup';
const messages = defineMessages('components.Login', {
loginwithapp: 'Login with {appName}',
username: 'Username',
email: 'Email Address',
password: 'Password',
@@ -51,7 +53,6 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
password: '',
}}
validationSchema={LoginSchema}
validateOnBlur={false}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/local', {
@@ -75,27 +76,21 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
{({ errors, touched, isSubmitting, isValid }) => {
return (
<>
<Form data-form-type="login">
<Form>
<div>
<h2 className="mb-6 -mt-1 text-center text-lg font-bold text-neutral-200">
{intl.formatMessage(messages.loginwithapp, {
appName: settings.currentSettings.applicationTitle,
})}
</h2>
<div className="mt-1 mb-4">
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email) +
' / ' +
intl.formatMessage(messages.username)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="form-input-field">
<Field
id="email"
name="email"
placeholder={`${intl.formatMessage(
messages.email
)} / ${intl.formatMessage(messages.username)}`}
type="text"
inputMode="email"
data-testid="email"
data-form-type="username,email"
className="!bg-gray-700/80 placeholder:text-gray-400"
/>
</div>
{errors.email &&
@@ -104,39 +99,25 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
<div className="error">{errors.email}</div>
)}
</div>
<div className="mt-1 mb-2">
<label htmlFor="password" className="text-label">
{intl.formatMessage(messages.password)}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="form-input-field">
<SensitiveInput
as="field"
id="password"
name="password"
type="password"
placeholder={intl.formatMessage(messages.password)}
autoComplete="current-password"
data-testid="password"
data-form-type="password"
className="!bg-gray-700/80 placeholder:text-gray-400"
data-1pignore="false"
data-lpignore="false"
data-bwignore="false"
/>
</div>
<div className="flex">
{errors.password &&
touched.password &&
typeof errors.password === 'string' && (
<div className="error">{errors.password}</div>
)}
<div className="flex-grow"></div>
{passwordResetEnabled && (
<Link
href="/resetpassword"
className="pt-2 text-sm text-indigo-500 hover:text-indigo-400"
>
{intl.formatMessage(messages.forgotpassword)}
</Link>
{errors.password &&
touched.password &&
typeof errors.password === 'string' && (
<div className="error">{errors.password}</div>
)}
</div>
</div>
{loginError && (
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
@@ -144,21 +125,37 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
</div>
)}
</div>
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
data-testid="local-signin-button"
className="mt-2 w-full shadow-sm"
>
<ArrowLeftOnRectangleIcon />
<span>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</span>
</Button>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="flex flex-row-reverse justify-between">
<span className="inline-flex rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
data-testid="local-signin-button"
>
<ArrowLeftOnRectangleIcon />
<span>
{isSubmitting
? intl.formatMessage(messages.signingin)
: intl.formatMessage(messages.signin)}
</span>
</Button>
</span>
{passwordResetEnabled && (
<span className="inline-flex rounded-md shadow-sm">
<Link href="/resetpassword" passHref legacyBehavior>
<Button as="a" buttonType="ghost">
<LifebuoyIcon />
<span>
{intl.formatMessage(messages.forgotpassword)}
</span>
</Button>
</Link>
</span>
)}
</div>
</div>
</Form>
</>
);

View File

@@ -1,62 +0,0 @@
import PlexIcon from '@app/assets/services/plex.svg';
import Button from '@app/components/Common/Button';
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import usePlexLogin from '@app/hooks/usePlexLogin';
import defineMessages from '@app/utils/defineMessages';
import { FormattedMessage } from 'react-intl';
const messages = defineMessages('components.Login', {
loginwithapp: 'Login with {appName}',
});
interface PlexLoginButtonProps {
onAuthToken: (authToken: string) => void;
isProcessing?: boolean;
onError?: (message: string) => void;
large?: boolean;
}
const PlexLoginButton = ({
onAuthToken,
onError,
isProcessing,
large,
}: PlexLoginButtonProps) => {
const { loading, login } = usePlexLogin({ onAuthToken, onError });
return (
<Button
className="relative flex-1 border-[#cc7b19] bg-[rgba(204,123,25,0.3)] hover:border-[#cc7b19] hover:bg-[rgba(204,123,25,0.7)] disabled:opacity-50"
onClick={login}
disabled={loading || isProcessing}
data-testid="plex-login-button"
>
{loading && (
<div className="absolute right-0 mr-4 h-4 w-4">
<SmallLoadingSpinner />
</div>
)}
{large ? (
<FormattedMessage
{...messages.loginwithapp}
values={{
appName: <PlexIcon className="mt-[2px] ml-[0.35em] w-8" />,
}}
>
{(chunks) => (
<>
{chunks.map((c) =>
typeof c === 'string' ? <span>{c}</span> : c
)}
</>
)}
</FormattedMessage>
) : (
<PlexIcon className="w-8" />
)}
</Button>
);
};
export default PlexLoginButton;

View File

@@ -1,13 +1,9 @@
import EmbyLogo from '@app/assets/services/emby-icon-only.svg';
import JellyfinLogo from '@app/assets/services/jellyfin-icon.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import Button from '@app/components/Common/Button';
import Accordion from '@app/components/Common/Accordion';
import ImageFader from '@app/components/Common/ImageFader';
import PageTitle from '@app/components/Common/PageTitle';
import LanguagePicker from '@app/components/Layout/LanguagePicker';
import JellyfinLogin from '@app/components/Login/JellyfinLogin';
import LocalLogin from '@app/components/Login/LocalLogin';
import PlexLoginButton from '@app/components/Login/PlexLoginButton';
import PlexLoginButton from '@app/components/PlexLoginButton';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
@@ -16,10 +12,10 @@ import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server';
import { useRouter } from 'next/dist/client/router';
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
import useSWR from 'swr';
import JellyfinLogin from './JellyfinLogin';
const messages = defineMessages('components.Login', {
signin: 'Sign In',
@@ -27,21 +23,16 @@ const messages = defineMessages('components.Login', {
signinwithplex: 'Use your Plex account',
signinwithjellyfin: 'Use your {mediaServerName} account',
signinwithoverseerr: 'Use your {applicationTitle} account',
orsigninwith: 'Or sign in with',
});
const Login = () => {
const intl = useIntl();
const router = useRouter();
const settings = useSettings();
const { user, revalidate } = useUser();
const [error, setError] = useState('');
const [isProcessing, setProcessing] = useState(false);
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const [mediaServerLogin, setMediaServerLogin] = useState(
settings.currentSettings.mediaServerLogin
);
const { user, revalidate } = useUser();
const router = useRouter();
const settings = useSettings();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to sign in. If we get a success message, we will
@@ -95,73 +86,14 @@ const Login = () => {
revalidateOnFocus: false,
});
const mediaServerName =
settings.currentSettings.mediaServerType === MediaServerType.PLEX
? 'Plex'
: settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined;
const MediaServerLogo =
settings.currentSettings.mediaServerType === MediaServerType.PLEX
? PlexLogo
: settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? JellyfinLogo
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? EmbyLogo
: undefined;
const isJellyfin =
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN ||
settings.currentSettings.mediaServerType === MediaServerType.EMBY;
const mediaServerLoginRef = useRef<HTMLDivElement>(null);
const localLoginRef = useRef<HTMLDivElement>(null);
const loginRef = mediaServerLogin ? mediaServerLoginRef : localLoginRef;
const loginFormVisible =
(isJellyfin && settings.currentSettings.mediaServerLogin) ||
settings.currentSettings.localLogin;
const additionalLoginOptions = [
settings.currentSettings.mediaServerLogin &&
(settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
<PlexLoginButton
key="plex"
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
large={!isJellyfin && !settings.currentSettings.localLogin}
/>
) : (
settings.currentSettings.localLogin &&
(mediaServerLogin ? (
<Button
key="jellyseerr"
data-testid="jellyseerr-login-button"
className="flex-1 bg-transparent"
onClick={() => setMediaServerLogin(false)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/os_icon.svg"
alt={settings.currentSettings.applicationTitle}
className="mr-2 h-5"
/>
<span>{settings.currentSettings.applicationTitle}</span>
</Button>
) : (
<Button
key="mediaserver"
data-testid="mediaserver-login-button"
className="flex-1 bg-transparent"
onClick={() => setMediaServerLogin(true)}
>
<MediaServerLogo />
<span>{mediaServerName}</span>
</Button>
))
)),
].filter((o): o is JSX.Element => !!o);
const mediaServerFormatValues = {
mediaServerName:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: undefined,
};
return (
<div className="relative flex min-h-screen flex-col bg-gray-900 py-14">
@@ -180,6 +112,9 @@ const Login = () => {
<div className="relative h-48 w-full max-w-full">
<Image src="/logo_stacked.svg" alt="Logo" fill />
</div>
<h2 className="mt-12 text-center text-3xl font-extrabold leading-9 text-gray-100">
{intl.formatMessage(messages.signinheader)}
</h2>
</div>
<div className="relative z-50 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div
@@ -210,71 +145,65 @@ const Login = () => {
</div>
</div>
</Transition>
<div className="px-10 py-8">
<SwitchTransition mode="out-in">
<CSSTransition
key={mediaServerLogin ? 'ms' : 'local'}
nodeRef={loginRef}
addEndListener={(done) => {
loginRef.current?.addEventListener(
'transitionend',
done,
false
);
}}
onEntered={() => {
document
.querySelector<HTMLInputElement>('#email, #username')
?.focus();
}}
classNames={{
appear: 'opacity-0',
appearActive: 'transition-opacity duration-500 opacity-100',
enter: 'opacity-0',
enterActive: 'transition-opacity duration-500 opacity-100',
exitActive: 'transition-opacity duration-0 opacity-0',
}}
>
<div ref={loginRef} className="button-container">
{isJellyfin &&
(mediaServerLogin ||
!settings.currentSettings.localLogin) ? (
<JellyfinLogin
serverType={settings.currentSettings.mediaServerType}
revalidate={revalidate}
/>
) : (
settings.currentSettings.localLogin && (
<LocalLogin revalidate={revalidate} />
)
)}
</div>
</CSSTransition>
</SwitchTransition>
{additionalLoginOptions.length > 0 &&
(loginFormVisible ? (
<div className="flex items-center py-5">
<div className="flex-grow border-t border-gray-600"></div>
<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>
) : (
<h2 className="mb-6 text-center text-lg font-bold text-neutral-200">
{intl.formatMessage(messages.signinheader)}
</h2>
))}
<div
className={`flex w-full flex-wrap gap-2 ${
!loginFormVisible ? 'flex-col' : ''
}`}
>
{additionalLoginOptions}
</div>
</div>
<Accordion single atLeastOne>
{({ openIndexes, handleClick, AccordionContent }) => (
<>
<button
className={`w-full cursor-default bg-gray-800 bg-opacity-70 py-2 text-center text-sm font-bold text-gray-400 transition-colors duration-200 focus:outline-none sm:rounded-t-lg ${
openIndexes.includes(0) && 'text-indigo-500'
} ${
settings.currentSettings.localLogin &&
'hover:cursor-pointer hover:bg-gray-700'
}`}
onClick={() => handleClick(0)}
disabled={!settings.currentSettings.localLogin}
>
{settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(
messages.signinwithjellyfin,
mediaServerFormatValues
)}
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8">
{settings.currentSettings.mediaServerType ==
MediaServerType.PLEX ? (
<PlexLoginButton
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
/>
) : (
<JellyfinLogin revalidate={revalidate} />
)}
</div>
</AccordionContent>
{settings.currentSettings.localLogin && (
<div>
<button
className={`w-full cursor-default bg-gray-800 bg-opacity-70 py-2 text-center text-sm font-bold text-gray-400 transition-colors duration-200 hover:cursor-pointer hover:bg-gray-700 focus:outline-none ${
openIndexes.includes(1)
? 'text-indigo-500'
: 'sm:rounded-b-lg'
}`}
onClick={() => handleClick(1)}
>
{intl.formatMessage(messages.signinwithoverseerr, {
applicationTitle:
settings.currentSettings.applicationTitle,
})}
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div className="px-10 py-8">
<LocalLogin revalidate={revalidate} />
</div>
</AccordionContent>
</div>
)}
</>
)}
</Accordion>
</>
</div>
</div>

View File

@@ -122,13 +122,15 @@ const ManageSlideOver = ({
const deleteMediaFile = async () => {
if (data.mediaInfo) {
// we don't check if the response is ok here because there may be no file to delete
await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, {
const res1 = await fetch(`/api/v1/media/${data.mediaInfo.id}/file`, {
method: 'DELETE',
});
await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
if (!res1.ok) throw new Error();
const res2 = await fetch(`/api/v1/media/${data.mediaInfo.id}`, {
method: 'DELETE',
});
if (!res2.ok) throw new Error();
revalidate();
onClose();

View File

@@ -56,7 +56,6 @@ const MediaSlider = ({
},
{
initialSize: 2,
revalidateFirstPage: false,
}
);

View File

@@ -25,7 +25,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers';
@@ -38,14 +38,14 @@ import {
ExclamationTriangleIcon,
EyeSlashIcon,
FilmIcon,
MinusCircleIcon,
PlayIcon,
StarIcon,
TicketIcon,
} from '@heroicons/react/24/outline';
import {
ChevronDoubleDownIcon,
ChevronDoubleUpIcon,
MinusCircleIcon,
StarIcon,
} from '@heroicons/react/24/solid';
import { type RatingResponse } from '@server/api/ratings';
import { IssueStatus } from '@server/constants/issue';
@@ -102,7 +102,7 @@ const messages = defineMessages('components.MovieDetails', {
watchlistSuccess: '<strong>{title}</strong> added to watchlist successfully!',
watchlistDeleted:
'<strong>{title}</strong> Removed from watchlist successfully!',
watchlistError: 'Something went wrong. Please try again.',
watchlistError: 'Something went wrong try again.',
removefromwatchlist: 'Remove From Watchlist',
addtowatchlist: 'Add To Watchlist',
});
@@ -190,7 +190,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
})
) {
mediaLinks.push({
text: getAvailableMediaServerName(),
text: getAvalaibleMediaServerName(),
url: plexUrl,
svg: <PlayIcon />,
});
@@ -204,7 +204,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
})
) {
mediaLinks.push({
text: getAvailable4kMediaServerName(),
text: getAvalaible4kMediaServerName(),
url: plexUrl4k,
svg: <PlayIcon />,
});
@@ -292,7 +292,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
(provider) => provider.iso_3166_1 === streamingRegion
)?.flatrate ?? [];
function getAvailableMediaServerName() {
function getAvalaibleMediaServerName() {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
@@ -304,7 +304,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
}
function getAvailable4kMediaServerName() {
function getAvalaible4kMediaServerName() {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
@@ -505,7 +505,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
src={
data.posterPath
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
: '/images/jellyseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png'
}
alt=""
sizes="100vw"
@@ -594,45 +594,42 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</Button>
</Tooltip>
)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED &&
user?.userType !== UserType.PLEX && (
<>
{toggleWatchlist ? (
<Tooltip
content={intl.formatMessage(messages.addtowatchlist)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
<>
{toggleWatchlist ? (
<Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
{isUpdating ? (
<Spinner className="h-3" />
) : (
<StarIcon className={'h-3 text-amber-300'} />
)}
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(messages.removefromwatchlist)}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
<Button
className="z-40 mr-2"
buttonSize={'md'}
onClick={onClickDeleteWatchlistBtn}
>
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
{isUpdating ? (
<Spinner className="h-3" />
) : (
<MinusCircleIcon className={'h-3'} />
)}
</Button>
</Tooltip>
)}
</>
)}
<PlayButton links={mediaLinks} />
<RequestButton
mediaType="movie"

View File

@@ -2,7 +2,7 @@ interface PWAHeaderProps {
applicationTitle?: string;
}
const PWAHeader = ({ applicationTitle = 'Jellyseerr' }: PWAHeaderProps) => {
const PWAHeader = ({ applicationTitle = 'Overseerr' }: PWAHeaderProps) => {
return (
<>
<link

Some files were not shown because too many files have changed in this diff Show More