Compare commits

...

8 Commits

Author SHA1 Message Date
Gauthier
6a100de2ec fix(emby): change default value of Accept-Encoding header 2024-12-10 20:02:05 +01:00
Gauthier
89831f7090 feat(usersettings): add separate setting for streaming region (#993)
* feat: add separate setting for streaming region

Currently, the "Currently Streaming On" information is based on the Discover Region setting. This PR
adds a new setting to specify which region should be used to display the streaming region.

re #890

* fix: add missing newline

* fix: rename migration function
2024-12-08 17:19:11 +01:00
Gauthier
84fd884052 fix: use tmdb first as metadata provider and fallback to tvdb (#1138)
* fix: use tmdb first as metadata provider and fallback to tvdb

This PR changes the order of the metadata provider to TMDB first and then fallback to TheTVDB if no
TMDB metadata is available. Previously, TheTVDB was used first and there was no fallback if the
latter failed.

fix #1137

* feat: add logs

* fix: add logs

* fix: add show name
2024-12-08 15:54:53 +01:00
Gauthier
57767156f7 fix: use links instead of buttons for external links in movie/tv details page (#923)
Previously, the "Play on Jellyfin" or "Watch Trailers" buttons used the onClick event and
window.open to open links, instead of using 'a' elements with a href.
2024-12-08 13:10:44 +01:00
Gauthier
9fa47cbba2 docs: add a troubleshooting page (#1109)
* docs: add a troubleshooting page

This troubleshooting page explains how to solve dns blockage issues from ISPs and how to fix some
slowdowns / ipv6 errors with the forceIpv4First environment variable.

fix #1098

* fix: specify the difference between DNS blocking and total blocking from the ISP
2024-12-06 06:03:41 +08:00
Gauthier
17418f82af fix(avatarproxy): add support for Emby avatars (#1128)
Refactoring avatarproxy to retrieve avatars from the Jellyfin API instead of the public endpoint
broke Emby avatars that doesn't have this API method.

fix #1101
2024-12-03 10:53:23 +01:00
Chris Bannister
01bbeced65 fix(server/settings): write settings to a temp file then move to avoid corruption (#1067)
When writing the settings.json file ensure that the file is fully written by writing it to temporary
file before renaming it to the final settings path. This should prevent issues where the config gets
lost due to the file being corrupted.
2024-11-27 10:42:30 +01:00
Ludovic Ortega
27e3d465bd feat(helm): add base helm chart (#1116)
* feat(helm): add base helm chart

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* chore(ci): ignore helm charts files in prettier

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* chore(ci): ignore helm charts files in prettier

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* chore(ci): prettier ignore charts folder

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: missing capital J

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

---------

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-11-26 16:24:30 +01:00
47 changed files with 1064 additions and 116 deletions

33
.github/workflows/lint-helm-charts.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Lint and Test Charts
on:
pull_request:
branches:
- develop
paths:
- '.github/workflows/lint-helm-charts.yml'
- 'charts/**'
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
- name: Ensure documentation is updated
uses: docker://jnorwood/helm-docs:v1.14.2
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.1
- name: Run chart-testing (list-changed)
id: list-changed
run: |
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
if [[ -n "$changed" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Run chart-testing
if: steps.list-changed.outputs.changed == 'true'
run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false

View File

@@ -4,7 +4,7 @@ on:
pull_request:
branches:
- develop
path:
paths:
- 'docs/**'
- 'gen-docs/**'

View File

@@ -9,3 +9,6 @@ pnpm-lock.yaml
src/assets/
public/
docs/
# helm charts
**/charts

View File

@@ -15,5 +15,11 @@ module.exports = {
rangeEnd: 0, // default: Infinity
},
},
{
files: 'charts/**',
options: {
rangeEnd: 0, // default: Infinity
},
},
],
};

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

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

View File

@@ -0,0 +1,69 @@
# Jellyseerr
![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
**Homepage:** <https://github.com/Fallenbagel/jellyseerr>
## Maintainers
| Name | Email | Url |
| ---- | ------ | --- |
| Jellyseerr | | <https://github.com/Fallenbagel/jellyseerr> |
## Source Code
* <https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr>
## Requirements
Kubernetes: `>=1.23.0-0`
## Values
| 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 |
| config.persistence.name | string | `""` | Config name |
| config.persistence.size | string | `"5Gi"` | Size of persistent disk |
| config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. |
| extraEnv | list | `[]` | Environment variables to add to the jellyseerr pods |
| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods |
| fullnameOverride | string | `""` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| 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. |
| imagePullSecrets | list | `[]` | |
| ingress.annotations | object | `{}` | |
| ingress.enabled | bool | `false` | |
| ingress.hosts[0].host | string | `"chart-example.local"` | |
| ingress.hosts[0].paths[0].path | string | `"/"` | |
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
| ingress.ingressClassName | string | `""` | |
| ingress.tls | list | `[]` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| podLabels | object | `{}` | |
| podSecurityContext | object | `{}` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| securityContext | object | `{}` | |
| service.port | int | `80` | |
| service.type | string | `"ClusterIP"` | |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
| 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 | `[]` | |

View File

@@ -0,0 +1,17 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}

View File

@@ -0,0 +1,5 @@
***********************************************************************
Welcome to {{ .Chart.Name }}
Chart version: {{ .Chart.Version }}
App version: {{ .Chart.AppVersion }}
***********************************************************************

View File

@@ -0,0 +1,70 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "jellyseerr.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "jellyseerr.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "jellyseerr.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "jellyseerr.labels" -}}
helm.sh/chart: {{ include "jellyseerr.chart" . }}
{{ include "jellyseerr.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/part-of: {{ .Chart.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "jellyseerr.selectorLabels" -}}
app.kubernetes.io/name: {{ include "jellyseerr.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "jellyseerr.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "jellyseerr.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Create the name of the pvc config to use
*/}}
{{- define "jellyseerr.configPersistenceName" -}}
{{- default (printf "%s-config" (include "jellyseerr.fullname" .)) .Values.config.persistence.name }}
{{- end }}

View File

@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "jellyseerr.fullname" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
type: {{ .Values.strategy.type }}
selector:
matchLabels:
{{- include "jellyseerr.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "jellyseerr.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "jellyseerr.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.image.sha }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}"
{{- else }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 5055
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.extraEnv }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: config
mountPath: /app/config
volumes:
- name: config
persistentVolumeClaim:
claimName: {{ include "jellyseerr.configPersistenceName" . }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

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

@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "jellyseerr.fullname" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "jellyseerr.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,20 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "jellyseerr.configPersistenceName" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
spec:
{{- with .Values.config.persistence.accessModes }}
accessModes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.config.persistence.volumeName }}
volumeName: {{ .Values.config.persistence.volumeName }}
{{- end }}
{{- with .Values.config.persistence.storageClass }}
storageClassName: {{ if (eq "-" .) }}""{{ else }}{{ . }}{{ end }}
{{- end }}
resources:
requests:
storage: "{{ .Values.config.persistence.size }}"

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "jellyseerr.fullname" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "jellyseerr.selectorLabels" . | nindent 4 }}
ipFamilyPolicy: PreferDualStack

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "jellyseerr.serviceAccountName" . }}
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "jellyseerr.fullname" . }}-test-connection"
labels:
{{- include "jellyseerr.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "jellyseerr.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,108 @@
replicaCount: 1
image:
registry: docker.io
repository: fallenbagel/jellyseerr
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
tag: ""
sha: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Deployment strategy
strategy:
type: Recreate
# -- Environment variables to add to the jellyseerr pods
extraEnv: []
# -- Environment variables from secrets or configmaps to add to the jellyseerr pods
extraEnvFrom: []
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Automatically mount a ServiceAccount's API credentials?
automount: true
# -- Annotations to add to the service account
annotations: {}
# -- The name of the service account to use.
# -- If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
# -- Creating PVC to store configuration
config:
persistence:
# -- Size of persistent disk
size: 5Gi
# -- Annotations for PVCs
annotations: {}
# -- Access modes of persistent disk
accessModes:
- ReadWriteOnce
# -- Config name
name: ""
# -- Name of the permanent volume to reference in the claim.
# Can be used to bind to existing volumes.
volumeName: ""
ingress:
enabled: false
ingressClassName: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -16,7 +16,8 @@
"hideAvailable": false,
"localLogin": true,
"newPlexLogin": true,
"region": "",
"discoverRegion": "",
"streamingRegion": "",
"originalLanguage": "",
"trustProxy": false,
"mediaServerType": 1,

158
docs/troubleshooting.mdx Normal file
View File

@@ -0,0 +1,158 @@
---
title: Troubleshooting
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## [TMDB] failed to retrieve/fetch XXX
### Option 1: Change your DNS servers
This error often comes from your Internet Service Provider (ISP) blocking TMDB API. The ISP may block the DNS resolution to the TMDB API hostname.
To fix this, you can change your DNS servers to a public DNS service like Google's DNS or Cloudflare's DNS:
<Tabs groupId="methods" queryString>
<TabItem value="docker-cli" label="Docker CLI">
Add the following to your `docker run` command to use Google's DNS:
```bash
--dns=8.8.8.8
```
or for Cloudflare's DNS:
```bash
--dns=1.1.1.1
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
Add the following to your `compose.yaml` to use Google's DNS:
```yaml
---
services:
jellyseerr:
dns:
- 8.8.8.8
```
or for Cloudflare's DNS:
```yaml
---
services:
jellyseerr:
dns:
- 1.1.1.1
```
</TabItem>
<TabItem value="windows" label="Windows">
1. Open the Control Panel.
2. Click on Network and Internet.
3. Click on Network and Sharing Center.
4. Click on Change adapter settings.
5. Right-click the network interface connected to the internet and select Properties.
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS.
</TabItem>
<TabItem value="linux" label="Linux">
1. Open a terminal.
2. Edit the `/etc/resolv.conf` file with your favorite text editor.
3. Add the following line to use Google's DNS:
```bash
nameserver 8.8.8.8
```
or for Cloudflare's DNS:
```bash
nameserver 1.1.1.1
```
</TabItem>
</Tabs>
### Option 2: Force IPV4 resolution first
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`:
<Tabs groupId="methods" queryString>
<TabItem value="docker-cli" label="Docker CLI">
Add the following to your `docker run` command:
```bash
-e "FORCE_IPV4_FIRST=true"
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
Add the following to your `compose.yaml`:
```yaml
---
services:
jellyseerr:
environment:
- FORCE_IPV4_FIRST=true
```
</TabItem>
</Tabs>
### Option 3: Use Jellyseerr through a proxy
If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy.
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
### Option 4: Check that your server can reach TMDB API
Make sure that your server can reach the TMDB API by running the following command:
<Tabs groupId="methods" queryString>
<TabItem value="docker-cli" label="Docker CLI">
```bash
docker exec -it jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
```bash
docker compose exec jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
```
</TabItem>
<TabItem value="linux" label="Linux">
In a terminal:
```bash
curl -L https://api.themoviedb.org
```
</TabItem>
<TabItem value="windows" label="Windows">
In a PowerShell window:
```powershell
(Invoke-WebRequest -Uri "https://api.themoviedb.org" -Method Get).Content
```
</TabItem>
</Tabs>
If you can't get a response, then your server can't reach the TMDB API.
This is usually due to a network configuration issue or a firewall blocking the connection.

View File

@@ -58,9 +58,9 @@ You should enable this if you are having issues with loading images directly fro
Set the default display language for Jellyseerr. Users can override this setting in their user settings.
## Discover Region & Discover Language
## Discover Region, Discover Language & Streaming Region
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings.
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
## Hide Available Media

View File

@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
### Discover Region & Discover Language
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region--discover-language) to suit their own preferences.
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-discover-language--streaming-region) to suit their own preferences.
### Movie Request Limit & Series Request Limit

View File

@@ -143,10 +143,12 @@ components:
properties:
locale:
type: string
region:
discoverRegion:
type: string
originalLanguage:
type: string
streamingRegion:
type: string
MainSettings:
type: object
properties:

View File

@@ -1,3 +1,5 @@
import { MediaServerType } from '@server/constants/server';
import { getSettings } from '@server/lib/settings';
import type { RateLimitOptions } from '@server/utils/rateLimit';
import rateLimit from '@server/utils/rateLimit';
import type NodeCache from 'node-cache';
@@ -34,6 +36,8 @@ class ExternalAPI {
const url = new URL(baseUrl);
const settings = getSettings();
this.defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
@@ -42,6 +46,9 @@ class ExternalAPI {
`${url.username}:${url.password}`
).toString('base64')}`,
}),
...(settings.main.mediaServerType === MediaServerType.EMBY && {
'Accept-Encoding': 'gzip',
}),
...options.headers,
};

View File

@@ -99,12 +99,12 @@ interface DiscoverTvOptions {
}
class TheMovieDb extends ExternalAPI {
private region?: string;
private discoverRegion?: string;
private originalLanguage?: string;
constructor({
region,
discoverRegion,
originalLanguage,
}: { region?: string; originalLanguage?: string } = {}) {
}: { discoverRegion?: string; originalLanguage?: string } = {}) {
super(
'https://api.themoviedb.org/3',
{
@@ -118,7 +118,7 @@ class TheMovieDb extends ExternalAPI {
},
}
);
this.region = region;
this.discoverRegion = discoverRegion;
this.originalLanguage = originalLanguage;
}
@@ -469,7 +469,7 @@ class TheMovieDb extends ExternalAPI {
page: page.toString(),
include_adult: includeAdult ? 'true' : 'false',
language,
region: this.region || '',
region: this.discoverRegion || '',
with_original_language:
originalLanguage && originalLanguage !== 'all'
? originalLanguage
@@ -541,7 +541,7 @@ class TheMovieDb extends ExternalAPI {
sort_by: sortBy,
page: page.toString(),
language,
region: this.region || '',
region: this.discoverRegion || '',
// Set our release date values, but check if one is set and not the other,
// so we can force a past date or a future date. TMDB Requires both values if one is set!
'first_air_date.gte':
@@ -594,7 +594,7 @@ class TheMovieDb extends ExternalAPI {
{
page: page.toString(),
language,
region: this.region || '',
region: this.discoverRegion || '',
originalLanguage: this.originalLanguage || '',
}
);
@@ -620,7 +620,7 @@ class TheMovieDb extends ExternalAPI {
{
page: page.toString(),
language,
region: this.region || '',
region: this.discoverRegion || '',
}
);

View File

@@ -31,7 +31,10 @@ export class UserSettings {
public locale?: string;
@Column({ nullable: true })
public region?: string;
public discoverRegion?: string;
@Column({ nullable: true })
public streamingRegion?: string;
@Column({ nullable: true })
public originalLanguage?: string;

View File

@@ -32,7 +32,8 @@ export interface PublicSettingsResponse {
localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
region: string;
discoverRegion: string;
streamingRegion: string;
originalLanguage: string;
mediaServerType: number;
partialRequestsEnabled: boolean;

View File

@@ -5,7 +5,8 @@ export interface UserSettingsGeneralResponse {
email?: string;
discordId?: string;
locale?: string;
region?: string;
discoverRegion?: string;
streamingRegion?: string;
originalLanguage?: string;
movieQuotaLimit?: number;
movieQuotaDays?: number;

View File

@@ -210,14 +210,27 @@ class JellyfinScanner {
return;
}
if (metadata.ProviderIds.Tvdb) {
tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: Number(metadata.ProviderIds.Tvdb),
});
} else if (metadata.ProviderIds.Tmdb) {
tvShow = await this.tmdb.getTvShow({
tvId: Number(metadata.ProviderIds.Tmdb),
});
if (metadata.ProviderIds.Tmdb) {
try {
tvShow = await this.tmdb.getTvShow({
tvId: Number(metadata.ProviderIds.Tmdb),
});
} catch {
this.log('Unable to find TMDb ID for this title.', 'debug', {
jellyfinitem,
});
}
}
if (!tvShow && metadata.ProviderIds.Tvdb) {
try {
tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: Number(metadata.ProviderIds.Tvdb),
});
} catch {
this.log('Unable to find TVDb ID for this title.', 'debug', {
jellyfinitem,
});
}
}
if (tvShow) {
@@ -491,7 +504,13 @@ class JellyfinScanner {
}
});
} else {
this.log(`failed show: ${metadata.Name}`);
this.log(
`No information found for the show: ${metadata.Name}`,
'debug',
{
jellyfinitem,
}
);
}
} catch (e) {
this.log(

View File

@@ -124,7 +124,8 @@ export interface MainSettings {
hideAvailable: boolean;
localLogin: boolean;
newPlexLogin: boolean;
region: string;
discoverRegion: string;
streamingRegion: string;
originalLanguage: string;
trustProxy: boolean;
mediaServerType: number;
@@ -144,7 +145,8 @@ interface FullPublicSettings extends PublicSettings {
localLogin: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
region: string;
discoverRegion: string;
streamingRegion: string;
originalLanguage: string;
mediaServerType: number;
jellyfinExternalHost?: string;
@@ -333,7 +335,8 @@ class Settings {
hideAvailable: false,
localLogin: true,
newPlexLogin: true,
region: '',
discoverRegion: '',
streamingRegion: '',
originalLanguage: '',
trustProxy: false,
mediaServerType: MediaServerType.NOT_CONFIGURED,
@@ -576,7 +579,8 @@ class Settings {
series4kEnabled: this.data.sonarr.some(
(sonarr) => sonarr.is4k && sonarr.isDefault
),
region: this.data.main.region,
discoverRegion: this.data.main.discoverRegion,
streamingRegion: this.data.main.streamingRegion,
originalLanguage: this.data.main.originalLanguage,
mediaServerType: this.main.mediaServerType,
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
@@ -688,10 +692,9 @@ class Settings {
}
public async save(): Promise<void> {
await fs.writeFile(
SETTINGS_PATH,
JSON.stringify(this.data, undefined, ' ')
);
const tmp = SETTINGS_PATH + '.tmp';
await fs.writeFile(tmp, JSON.stringify(this.data, undefined, ' '));
await fs.rename(tmp, SETTINGS_PATH);
}
}

View File

@@ -0,0 +1,17 @@
import type { AllSettings } from '@server/lib/settings';
const migrateRegionSetting = (settings: any): AllSettings => {
const oldRegion = settings.main.region;
if (oldRegion) {
settings.main.discoverRegion = oldRegion;
settings.main.streamingRegion = oldRegion;
} else {
settings.main.discoverRegion = '';
settings.main.streamingRegion = 'US';
}
delete settings.main.region;
return settings;
};
export default migrateRegionSetting;

View File

@@ -0,0 +1,53 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsStreamingRegion1727907530757
implements MigrationInterface
{
name = 'AddUserSettingsStreamingRegion1727907530757';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "discoverRegion" varchar, "streamingRegion" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "pushoverSound" varchar, "region" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound") SELECT "id", "notificationTypes", "discordId", "userId", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "watchlistSyncMovies", "watchlistSyncTv", "pushoverSound" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

View File

@@ -54,9 +54,15 @@ router.get('/:jellyfinUserId', async (req, res) => {
default: 'mm',
size: 200,
});
const jellyfinAvatarUrl = `${getHostname()}/UserImage?UserId=${
req.params.jellyfinUserId
}`;
const setttings = getSettings();
const jellyfinAvatarUrl =
setttings.main.mediaServerType === MediaServerType.JELLYFIN
? `${getHostname()}/UserImage?UserId=${req.params.jellyfinUserId}`
: `${getHostname()}/Users/${
req.params.jellyfinUserId
}/Images/Primary?quality=90`;
let imageData = await avatarImageCache.getImage(
jellyfinAvatarUrl,
fallbackUrl

View File

@@ -29,12 +29,12 @@ import { z } from 'zod';
export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
const settings = getSettings();
const region =
user?.settings?.region === 'all'
const discoverRegion =
user?.settings?.streamingRegion === 'all'
? ''
: user?.settings?.region
? user?.settings?.region
: settings.main.region;
: user?.settings?.streamingRegion
? user?.settings?.streamingRegion
: settings.main.discoverRegion;
const originalLanguage =
user?.settings?.originalLanguage === 'all'
@@ -44,7 +44,7 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
: settings.main.originalLanguage;
return new TheMovieDb({
region,
discoverRegion,
originalLanguage,
});
};

View File

@@ -57,7 +57,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
email: user.email,
discordId: user.settings?.discordId,
locale: user.settings?.locale,
region: user.settings?.region,
discoverRegion: user.settings?.discoverRegion,
streamingRegion: user.settings?.streamingRegion,
originalLanguage: user.settings?.originalLanguage,
movieQuotaLimit: user.movieQuotaLimit,
movieQuotaDays: user.movieQuotaDays,
@@ -147,7 +148,8 @@ userSettingsRoutes.post<
user: req.user,
discordId: req.body.discordId,
locale: req.body.locale,
region: req.body.region,
discoverRegion: req.body.discoverRegion,
streamingRegion: req.body.streamingRegion,
originalLanguage: req.body.originalLanguage,
watchlistSyncMovies: req.body.watchlistSyncMovies,
watchlistSyncTv: req.body.watchlistSyncTv,
@@ -155,7 +157,8 @@ userSettingsRoutes.post<
} else {
user.settings.discordId = req.body.discordId;
user.settings.locale = req.body.locale;
user.settings.region = req.body.region;
user.settings.discoverRegion = req.body.discoverRegion;
user.settings.streamingRegion = req.body.streamingRegion;
user.settings.originalLanguage = req.body.originalLanguage;
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
@@ -167,7 +170,8 @@ userSettingsRoutes.post<
username: savedUser.username,
discordId: savedUser.settings?.discordId,
locale: savedUser.settings?.locale,
region: savedUser.settings?.region,
discoverRegion: savedUser.settings?.discoverRegion,
streamingRegion: savedUser.settings?.streamingRegion,
originalLanguage: savedUser.settings?.originalLanguage,
watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies,
watchlistSyncTv: savedUser.settings?.watchlistSyncTv,

View File

@@ -2,7 +2,11 @@ import useClickOutside from '@app/hooks/useClickOutside';
import { withProperties } from '@app/utils/typeHelpers';
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';
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
@@ -35,23 +39,33 @@ const DropdownItem = ({
);
};
interface ButtonWithDropdownProps
extends ButtonHTMLAttributes<HTMLButtonElement> {
interface ButtonWithDropdownProps {
text: React.ReactNode;
dropdownIcon?: React.ReactNode;
buttonType?: 'primary' | 'ghost';
}
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>(null);
const buttonRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
useClickOutside(buttonRef, () => setIsOpen(false));
const styleClasses = {
@@ -78,16 +92,28 @@ const ButtonWithDropdown = ({
return (
<span className="relative inline-flex h-full rounded-md shadow-sm">
<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}
{...props}
>
{text}
</button>
{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">
<button

View File

@@ -17,6 +17,7 @@ const PlayButton = ({ links }: PlayButtonProps) => {
return (
<ButtonWithDropdown
as="a"
buttonType="ghost"
text={
<>
@@ -24,19 +25,17 @@ const PlayButton = ({ links }: PlayButtonProps) => {
<span>{links[0].text}</span>
</>
}
onClick={() => {
window.open(links[0].url, '_blank');
}}
href={links[0].url}
target="_blank"
>
{links.length > 1 &&
links.slice(1).map((link, i) => {
return (
<ButtonWithDropdown.Item
key={`play-button-dropdown-item-${i}`}
onClick={() => {
window.open(link.url, '_blank');
}}
buttonType="ghost"
href={link.url}
target="_blank"
>
{link.svg}
<span>{link.text}</span>

View File

@@ -222,14 +222,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
});
}
const region = user?.settings?.region
? user.settings.region
: settings.currentSettings.region
? settings.currentSettings.region
const discoverRegion = user?.settings?.discoverRegion
? user.settings.discoverRegion
: settings.currentSettings.discoverRegion
? settings.currentSettings.discoverRegion
: 'US';
const releases = data.releases.results.find(
(r) => r.iso_3166_1 === region
(r) => r.iso_3166_1 === discoverRegion
)?.release_dates;
// Release date types:
@@ -282,9 +282,15 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
);
}
const streamingRegion = user?.settings?.streamingRegion
? user.settings.streamingRegion
: settings.currentSettings.streamingRegion
? settings.currentSettings.streamingRegion
: 'US';
const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
data?.watchProviders?.find(
(provider) => provider.iso_3166_1 === streamingRegion
)?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {

View File

@@ -21,6 +21,7 @@ interface RegionSelectorProps {
isUserSetting?: boolean;
disableAll?: boolean;
watchProviders?: boolean;
regionType?: 'discover' | 'streaming';
onChange?: (fieldName: string, region: string) => void;
}
@@ -30,6 +31,7 @@ const RegionSelector = ({
isUserSetting = false,
disableAll = false,
watchProviders = false,
regionType = 'discover',
onChange,
}: RegionSelectorProps) => {
const { currentSettings } = useSettings();
@@ -63,6 +65,11 @@ const RegionSelector = ({
sortedRegions?.find((region) => region.iso_3166_1 === regionCode)?.name ??
regionCode;
const regionValue =
regionType === 'discover'
? currentSettings.discoverRegion
: currentSettings.streamingRegion;
useEffect(() => {
if (regions && value) {
if (value === 'all') {
@@ -97,14 +104,12 @@ const RegionSelector = ({
countries.includes(selectedRegion?.iso_3166_1)) ||
(isUserSetting &&
!selectedRegion &&
currentSettings.region &&
countries.includes(currentSettings.region))) && (
regionValue &&
countries.includes(regionValue))) && (
<span className="mr-2 h-4 overflow-hidden text-base leading-4">
<span
className={`flag:${
selectedRegion
? selectedRegion.iso_3166_1
: currentSettings.region
selectedRegion ? selectedRegion.iso_3166_1 : regionValue
}`}
/>
</span>
@@ -114,8 +119,8 @@ const RegionSelector = ({
? regionName(selectedRegion.iso_3166_1)
: isUserSetting && selectedRegion?.iso_3166_1 !== 'all'
? intl.formatMessage(messages.regionServerDefault, {
region: currentSettings.region
? regionName(currentSettings.region)
region: regionValue
? regionName(regionValue)
: intl.formatMessage(messages.regionDefault),
})
: intl.formatMessage(messages.regionDefault)}
@@ -148,8 +153,8 @@ const RegionSelector = ({
<span className="mr-2 text-base">
<span
className={
countries.includes(currentSettings.region)
? `flag:${currentSettings.region}`
countries.includes(regionValue)
? `flag:${regionValue}`
: 'pr-6'
}
/>
@@ -160,8 +165,8 @@ const RegionSelector = ({
} block truncate`}
>
{intl.formatMessage(messages.regionServerDefault, {
region: currentSettings.region
? regionName(currentSettings.region)
region: regionValue
? regionName(regionValue)
: intl.formatMessage(messages.regionDefault),
})}
</span>

View File

@@ -374,7 +374,11 @@ export const WatchProviderSelector = ({
const { currentSettings } = useSettings();
const [showMore, setShowMore] = useState(false);
const [watchRegion, setWatchRegion] = useState(
region ? region : currentSettings.region ? currentSettings.region : 'US'
region
? region
: currentSettings.discoverRegion
? currentSettings.discoverRegion
: 'US'
);
const [activeProvider, setActiveProvider] = useState<number[]>(
activeProviders ?? []

View File

@@ -31,10 +31,12 @@ const messages = defineMessages('components.Settings.SettingsMain', {
apikey: 'API Key',
applicationTitle: 'Application Title',
applicationurl: 'Application URL',
region: 'Discover Region',
regionTip: 'Filter content by regional availability',
discoverRegion: 'Discover Region',
discoverRegionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language',
originallanguageTip: 'Filter content by original language',
streamingRegion: 'Streaming Region',
streamingRegionTip: 'Show streaming sites by regional availability',
toastApiKeySuccess: 'New API key generated successfully!',
toastApiKeyFailure: 'Something went wrong while generating a new API key.',
toastSettingsSuccess: 'Settings saved successfully!',
@@ -152,8 +154,9 @@ const SettingsMain = () => {
csrfProtection: data?.csrfProtection,
hideAvailable: data?.hideAvailable,
locale: data?.locale ?? 'en',
region: data?.region,
discoverRegion: data?.discoverRegion,
originalLanguage: data?.originalLanguage,
streamingRegion: data?.streamingRegion,
partialRequestsEnabled: data?.partialRequestsEnabled,
trustProxy: data?.trustProxy,
cacheImages: data?.cacheImages,
@@ -181,7 +184,8 @@ const SettingsMain = () => {
csrfProtection: values.csrfProtection,
hideAvailable: values.hideAvailable,
locale: values.locale,
region: values.region,
discoverRegion: values.discoverRegion,
streamingRegion: values.streamingRegion,
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
@@ -402,17 +406,17 @@ const SettingsMain = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="region" className="text-label">
<span>{intl.formatMessage(messages.region)}</span>
<label htmlFor="discoverRegion" className="text-label">
<span>{intl.formatMessage(messages.discoverRegion)}</span>
<span className="label-tip">
{intl.formatMessage(messages.regionTip)}
{intl.formatMessage(messages.discoverRegionTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<RegionSelector
value={values.region ?? ''}
name="region"
value={values.discoverRegion ?? ''}
name="discoverRegion"
onChange={setFieldValue}
/>
</div>
@@ -434,6 +438,25 @@ const SettingsMain = () => {
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="streamingRegion" className="text-label">
<span>{intl.formatMessage(messages.streamingRegion)}</span>
<span className="label-tip">
{intl.formatMessage(messages.streamingRegionTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<RegionSelector
value={values.streamingRegion || 'US'}
name="streamingRegion"
onChange={setFieldValue}
regionType="streaming"
disableAll
/>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="hideAvailable" className="checkbox-label">
<span className="mr-2">

View File

@@ -222,15 +222,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
});
}
const region = user?.settings?.region
? user.settings.region
: settings.currentSettings.region
? settings.currentSettings.region
const discoverRegion = user?.settings?.discoverRegion
? user.settings.discoverRegion
: settings.currentSettings.discoverRegion
? settings.currentSettings.discoverRegion
: 'US';
const seriesAttributes: React.ReactNode[] = [];
const contentRating = data.contentRatings.results.find(
(r) => r.iso_3166_1 === region
(r) => r.iso_3166_1 === discoverRegion
)?.rating;
if (contentRating) {
seriesAttributes.push(
@@ -312,9 +312,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
(showHasSpecials ? seasonCount + 1 : seasonCount) <=
getAllRequestedSeasons(true).length;
const streamingRegion = user?.settings?.streamingRegion
? user.settings.streamingRegion
: settings.currentSettings.streamingRegion
? settings.currentSettings.streamingRegion
: 'US';
const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
data?.watchProviders?.find(
(provider) => provider.iso_3166_1 === streamingRegion
)?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) {

View File

@@ -48,8 +48,12 @@ const messages = defineMessages(
'Another user already has this username. You must set an email',
region: 'Discover Region',
regionTip: 'Filter content by regional availability',
discoverRegion: 'Discover Region',
discoverRegionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language',
originallanguageTip: 'Filter content by original language',
streamingRegion: 'Streaming Region',
streamingRegionTip: 'Show streaming sites by regional availability',
movierequestlimit: 'Movie Request Limit',
seriesrequestlimit: 'Series Request Limit',
enableOverride: 'Override Global Limit',
@@ -144,7 +148,8 @@ const UserGeneralSettings = () => {
email: data?.email?.includes('@') ? data.email : '',
discordId: data?.discordId ?? '',
locale: data?.locale,
region: data?.region,
discoverRegion: data?.discoverRegion,
streamingRegion: data?.streamingRegion,
originalLanguage: data?.originalLanguage,
movieQuotaLimit: data?.movieQuotaLimit,
movieQuotaDays: data?.movieQuotaDays,
@@ -168,7 +173,8 @@ const UserGeneralSettings = () => {
values.email || user?.jellyfinUsername || user?.plexUsername,
discordId: values.discordId,
locale: values.locale,
region: values.region,
discoverRegion: values.discoverRegion,
streamingRegion: values.streamingRegion,
originalLanguage: values.originalLanguage,
movieQuotaLimit: movieQuotaEnabled
? values.movieQuotaLimit
@@ -400,17 +406,17 @@ const UserGeneralSettings = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="displayName" className="text-label">
<span>{intl.formatMessage(messages.region)}</span>
<label htmlFor="discoverRegion" className="text-label">
<span>{intl.formatMessage(messages.discoverRegion)}</span>
<span className="label-tip">
{intl.formatMessage(messages.regionTip)}
{intl.formatMessage(messages.discoverRegionTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<RegionSelector
name="region"
value={values.region ?? ''}
name="discoverRegion"
value={values.discoverRegion ?? ''}
isUserSetting
onChange={setFieldValue}
/>
@@ -435,6 +441,26 @@ const UserGeneralSettings = () => {
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="streamingRegionTip" className="text-label">
<span>{intl.formatMessage(messages.streamingRegion)}</span>
<span className="label-tip">
{intl.formatMessage(messages.streamingRegionTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field">
<RegionSelector
name="streamingRegion"
value={values.streamingRegion || ''}
isUserSetting
onChange={setFieldValue}
regionType="streaming"
disableAll
/>
</div>
</div>
</div>
{currentHasPermission(Permission.MANAGE_USERS) &&
!hasPermission(Permission.MANAGE_USERS) && (
<>

View File

@@ -16,7 +16,8 @@ const defaultSettings = {
localLogin: true,
movie4kEnabled: false,
series4kEnabled: false,
region: '',
discoverRegion: '',
streamingRegion: '',
originalLanguage: '',
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,

View File

@@ -29,7 +29,8 @@ type NotificationAgentTypes = Record<NotificationAgentKey, number>;
export interface UserSettings {
discordId?: string;
region?: string;
discoverRegion?: string;
streamingRegion?: string;
originalLanguage?: string;
locale?: string;
notificationTypes: Partial<NotificationAgentTypes>;

View File

@@ -880,6 +880,8 @@
"components.Settings.SettingsMain.csrfProtection": "Enable CSRF Protection",
"components.Settings.SettingsMain.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
"components.Settings.SettingsMain.discoverRegion": "Discover Region",
"components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability",
"components.Settings.SettingsMain.general": "General",
"components.Settings.SettingsMain.generalsettings": "General Settings",
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
@@ -897,8 +899,8 @@
"components.Settings.SettingsMain.proxyPort": "Proxy Port",
"components.Settings.SettingsMain.proxySsl": "Use SSL For Proxy",
"components.Settings.SettingsMain.proxyUser": "Proxy Username",
"components.Settings.SettingsMain.region": "Discover Region",
"components.Settings.SettingsMain.regionTip": "Filter content by regional availability",
"components.Settings.SettingsMain.streamingRegion": "Streaming Region",
"components.Settings.SettingsMain.streamingRegionTip": "Show streaming sites by regional availability",
"components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.",
"components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!",
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",
@@ -1097,7 +1099,7 @@
"components.Setup.finishing": "Finishing…",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",
"components.Setup.signin": "Sign in to your account",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
@@ -1223,6 +1225,8 @@
"components.UserProfile.UserSettings.UserGeneralSettings.applanguage": "Display Language",
"components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord User ID",
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account",
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegion": "Discover Region",
"components.UserProfile.UserSettings.UserGeneralSettings.discoverRegionTip": "Filter content by regional availability",
"components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name",
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Email",
"components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Override Global Limit",
@@ -1246,6 +1250,8 @@
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Save Changes",
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Saving…",
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit",
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "Streaming Region",
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "Show streaming sites by regional availability",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmail": "This email is already taken!",
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailureEmailEmpty": "Another user already has this username. You must set an email",

View File

@@ -192,7 +192,8 @@ CoreApp.getInitialProps = async (initialProps) => {
movie4kEnabled: false,
series4kEnabled: false,
localLogin: true,
region: '',
discoverRegion: '',
streamingRegion: '',
originalLanguage: '',
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,