Compare commits

...

101 Commits

Author SHA1 Message Date
Fallenbagel
8feb20ff52 Merge pull request #157 from Fallenbagel/staging-for-1.1.1
chore(release): 1.1.1
2022-06-20 20:15:44 +05:00
Fallenbagel
f2c659c6f3 chore(release): 1.1.1 2022-06-20 20:11:05 +05:00
semantic-release-bot
99f1a4e4f3 chore(release): 1.2.0 2022-06-20 14:29:16 +00:00
Fallenbagel
fea9457dad Merge pull request #156 from Fallenbagel/develop
Merge branch 'develop' to fix the release workflow
2022-06-20 19:27:42 +05:00
Fallenbagel
883b9377be Merge pull request #155 from Fallenbagel/change-to-self-hosted-workflow
fix yarn dependecies install in release workflow
2022-06-20 19:26:09 +05:00
Fallenbagel
c7ba553208 fix yarn dependecies install in release workflow 2022-06-20 19:25:25 +05:00
Fallenbagel
76472521ed Merge pull request #154 from Fallenbagel/change-to-self-hosted-workflow
add yarn dependencies
2022-06-20 19:21:38 +05:00
Fallenbagel
a34e14b496 add yarn dependencies 2022-06-20 19:19:28 +05:00
Fallenbagel
23c9595933 Merge pull request #153 from Fallenbagel/develop
Merge branch 'develop'
2022-06-20 18:30:20 +05:00
Fallenbagel
715e229e01 Merge pull request #152 from Fallenbagel/change-to-self-hosted-workflow
Changed run-on to self-hosted to speed up release process
2022-06-20 17:39:43 +05:00
Fallenbagel
a5e6217f85 Changed run-on to self-hosted to speed up release process 2022-06-20 17:36:13 +05:00
Fallenbagel
af522516f7 Merge pull request #149 from jab416171/fix-library-scan-text
Fix text for manual library scan
2022-06-19 23:53:56 +05:00
Fallenbagel
647f594dc8 Merge pull request #147 from jab416171/develop
change email sender name to Jellyseerr on initial setup
2022-06-19 23:42:45 +05:00
jab416171
ae60d44f99 Fix text for manual library scan 2022-06-19 12:38:49 -06:00
jab416171
9275119163 change email sender name to Jellyseerr on initial setup 2022-06-19 12:28:09 -06:00
Fallenbagel
94b418bd47 Merge pull request #146 from Fallenbagel/fix-ui-description-mistakes
fix(setup&login): fix a description error in the manual scan in setup and add emby to login page
2022-06-19 22:57:33 +05:00
Fallenbagel
8810c20fc1 fix(setup&login): fix a description error in the manual scan in setup and add emby to login page
Manual scan in setup says "Jellyfin will scan your Jellyfin's server" and same with emby, so I
replaced jellyfin with jellyseerr. And also added emby to login page
2022-06-19 22:27:38 +05:00
Nicolai Van der Storm
63b7be0a38 Update private_registery_push.yml 2022-06-18 22:55:07 +02:00
Nicolai Van der Storm
d3cea69011 Update private_registery_push.yml 2022-06-18 22:48:47 +02:00
Nicolai Van der Storm
31072f4758 Update private_registery_push.yml 2022-06-18 22:45:43 +02:00
Nicolai Van der Storm
89e8825b61 fixes indent issues 2022-06-18 22:39:24 +02:00
Fallenbagel
7956ed8466 Merge pull request #144 from NicolaiVdS/private_registery_workflow
Create private_registery_push.yml
2022-06-19 01:34:08 +05:00
Nicolai Van der Storm
e1081a7bc2 Create private_registery_push.yml 2022-06-18 22:30:15 +02:00
Fallenbagel
fe3495705f Merge pull request #136 from NicolaiVdS/email-validation-and-requirement
feat(userprofile): email requirement and validation + import user button overhaul
2022-06-15 01:29:47 +05:00
Nicolai Van der Storm
29478fc195 fix(import all): fis for import all 2022-06-13 23:13:22 +02:00
Nicolai Van der Storm
f48286043e Merge pull request #139 from Fallenbagel/translation-issue
fix(ui): fixed translation issue where it showed as import {mediaServerName} user
2022-06-13 21:54:07 +02:00
Fallenbagel
d417fcafa1 fix(ui): replaced {mediaServerName} in the plex variable in NL locale
replaced {mediaServerName} in the plex variable in NL locale
2022-06-14 00:51:44 +05:00
Fallenbagel
819190ce98 fix(ui): fixed translation issue where it showed as import {mediaServerName} user
Fixed translation issue where it showed as import {mediaServerName} user as it was using the same
variable both inside the plex import modal and also outside in userlist on the button
2022-06-14 00:48:17 +05:00
Nicolai Van der Storm
a483ca9837 fix(jellyfinimportmodal): fix for importing all jellyfin users 2022-06-13 21:34:51 +02:00
Nicolai Van der Storm
d835336d33 feat(email validation): email requirement and validation + better importer 2022-06-13 14:21:05 +02:00
Nicolai Van der Storm
cc69f66ba9 chore(.idea folder): removed .idea folder and added it to the .gitignore 2022-06-10 12:30:14 +02:00
Nicolai Van der Storm
543859e6f3 feat(uesrprofile): email requirement and validation 2022-06-10 12:19:52 +02:00
Fallenbagel
4fd42874b7 Merge pull request #133 from Fallenbagel/fix-translation-errors
fix(ui): fix translation errors for all locales in the import plex user button
2022-06-09 16:51:56 +05:00
Fallenbagel
0fb5803eb9 fix(ui): fix translation errors for all locales in the import plex user button
fix translation errors for all locales in the import plex user button as it currently shows as
{mediaServerName}
2022-06-09 16:38:18 +05:00
Fallenbagel
00c08b3d67 Merge pull request #132 from notfakie/develop
fix: fix mediaServerType for plex users not being set properly
2022-06-09 16:25:48 +05:00
notfakie
94ade93e16 fix: fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings 2022-06-09 17:08:00 +12:00
Fallenbagel
caa713a968 Merge pull request #128 from NicolaiVdS/feature-add-email-field
feat: add email field in the profile settings
2022-06-05 21:53:53 +05:00
Nicolai Van der Storm
23779f4c7b style: removed .idea folder 2022-06-05 18:43:52 +02:00
Nicolai Van der Storm
5f2ebfe662 Revert "feat(tv): tv seasons"
This reverts commit c117b37cd9.
2022-06-05 18:37:50 +02:00
Nicolai Van der Storm
b22f20b6fa feat(user settings): added email field to user profiel settings
#122
2022-06-05 18:25:15 +02:00
Nicolai Van der Storm
a8bc0c068b feat: email
#122
2022-06-05 18:11:20 +02:00
Nicolai Van der Storm
30c48f16ca feat(user email setting): added field to save user email
fix #122
2022-06-05 17:46:26 +02:00
Fallenbagel
3748f64ce4 Merge pull request #127 from sambartik/fix-sync-errors
fix(jellyfin): fixes sync errors re-introduced in previous commits
2022-06-05 20:27:00 +05:00
Samuel Bartík
d1dbd6e3b9 fix(jellyfin): sync errors 2022-06-05 17:07:27 +02:00
Fallenbagel
6458c054c0 Merge pull request #126 from sambartik/fix-virtual-location
fix(jellyfin): ignore additional items with virtual location type
2022-06-05 18:25:01 +05:00
Samuel Bartík
c81154800f fix(jellyfin): ignore additional items with virtual location type 2022-06-05 12:41:34 +02:00
Fallenbagel
a1cd354691 Merge pull request #125 from sambartik/fix-scan
fix(scan): ignore virtual seasons
2022-06-05 12:35:13 +05:00
Samuel Bartík
6574e18516 fix(scan): ignore virtual seasons
Virtual seasons appeared as available / partially available, even though they were not even shown in the Jellyfin web UI. For more info see #119
2022-06-04 18:07:14 +02:00
Fallenbagel
5298e5fd90 Merge pull request #121 from notfakie/develop
Hide Overseerr settings when running in Jellyfin/Emby mode
2022-06-02 14:42:49 +05:00
notfakie
7450138ac1 fix: hide plex guid cache settings from ui when running in jellyfin/emby mode 2022-06-02 18:47:27 +12:00
notfakie
4b7bdd3d7d fix: remove internal Overseerr sponsor link, this is remaining on the main github page instead 2022-06-02 18:47:25 +12:00
notfakie
739f5f9c9a fix: only show mediaserver settings for current active mediaserver 2022-06-02 18:47:22 +12:00
Nicolai Van der Storm
c117b37cd9 feat(tv): tv seasons
tv seasons
2022-06-01 14:48:05 +02:00
Fallenbagel
3e7d64eb47 Merge pull request #120 from Fallenbagel/embySupport
fix(ui): fix emby ui elements not reflecting the env variable #98
2022-05-30 00:31:05 +05:00
Fallenbagel
b9546e6daa feat(ui): add emby user badge to the userProfile
adds emby user badge to the userProfile general page
2022-05-30 00:21:04 +05:00
Fallenbagel
722dda5856 fix(ui): fix ui elements not reflecting the env variable
Fix emby ui elements not reflecting the emby env variable set during runtime
2022-05-29 23:58:54 +05:00
Fallenbagel
c67ca34111 Merge pull request #118 from jab416171/patch-2
fix aur url
2022-05-29 05:50:09 +05:00
jab416171
16311808b1 fix aur url 2022-05-28 18:34:50 -06:00
Fallenbagel
509c43e552 Merge pull request #117 from jab416171/patch-1
Add manual install steps and aur package info
2022-05-29 05:29:49 +05:00
jab416171
84a97675dc Add manual install steps and aur package info 2022-05-28 18:14:57 -06:00
Fallenbagel
4615286f49 Merge pull request #115 from CyferShepard/JellyfinNoPasswordFix
feat:Remove Requirement for Jellyfin Passwords (#108 #31)
2022-05-28 12:32:02 +05:00
Fallenbagel
7b7354d006 Merge remote-tracking branch 'upstream/develop' into develop 2022-05-28 12:20:26 +05:00
CyferShepard
ad7b3590d7 Move auth.ts to correct folder 2022-05-28 08:50:51 +02:00
CyferShepard
bda7858b66 Delete auth.ts 2022-05-28 08:50:23 +02:00
CyferShepard
d600a45559 feat:Remove Requirement for Jellyfin Passwords 2022-05-28 08:29:45 +02:00
Fallenbagel
1dcfe49b1b Merge pull request #112 from Fallenbagel/forgotPasswordFix
fix: fixes jellyfin forgot password and adds emby support to the forgort password link
2022-05-28 07:29:46 +05:00
Fallenbagel
1dbc565a2e Merge pull request #103 from boring-dragon/develop
feat: conditional media server name to add emby
2022-05-28 07:29:38 +05:00
Fallenbagel
973a3e826f Merge pull request #111 from Fallenbagel/AvatarUrlFix
fix(ui): fix Avatar being broken when setup using internal ip
2022-05-28 07:22:31 +05:00
Fallenbagel
6a6bfe0c68 feat(ui): add emby as a mediaServerType to the import user button
Add emby as a mediaServerType to the import user button and replace any reference to Jellyfin with
mediaServerName
2022-05-27 05:35:00 +05:00
Fallenbagel
410b536c94 feat(ui): add emby user badge to the user list and fix local user badge
add emby user badge to the user list and fix local user badge which was previously not showing
2022-05-27 04:58:24 +05:00
Fallenbagel
0259975402 fix: fixes jellyfin forgot password and adds emby support to the forgot password link
Fixes jellyfin forgot password as it had a wrong url that lead to an empty page and also adds in
emby forgot password link if the environmental variable is set

fix #99
2022-05-26 10:11:53 +05:00
Fallenbagel
18d8d969f1 style(ui): conditional mediaServerName to add emby to setup/login page 2022-05-26 09:39:19 +05:00
Fallenbagel
f8a239b1b8 style(ui): conditional media server name to add emby to settings
Conditionaly media server name to replace every reference of jellyfin with emby in settings tab when
environmental variable set
2022-05-26 08:52:16 +05:00
Fallenbagel
377a4fd85b feat(ui): conditional media server name to add emby to issuedetails play on button 2022-05-26 08:39:39 +05:00
Fallenbagel
14d293799b feat(ui): conditional media server name to add emby to moviedetails 2022-05-26 08:37:51 +05:00
Fallenbagel
ddd773c03f fix: conditional media server name for 4k url to add emby to tvdetails 2022-05-26 08:30:21 +05:00
Fallenbagel
e75b71b816 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:54:57 +05:00
Fallenbagel
ff3e3ce841 feat: conditional media server name to add emby to tvdetails 2022-05-26 05:49:33 +05:00
Fallenbagel
01e81a73a3 fix(ui): fix Avatar being broken when setup using internal ip
allow avatar url to use externalHostname when setup using local ip

fix #110
2022-05-26 02:24:31 +05:00
Fallenbagel
71d33e47a9 Merge pull request #109 from sambartik/revert-104-fix-profilepic
Revert "Fix non-square profile pictures being squished"
2022-05-26 00:48:49 +05:00
Samuel Bartík
db05172d8b fix(ui): rectangular avatars getting stretched (#2782) 2022-05-25 20:42:17 +04:00
notfakie
93a9cff2ef Merge remote-tracking branch 'overseerr/develop' into up-develop 2022-05-25 18:34:49 +12:00
Samuel Bartík
e9fa94097a Revert "Fix non-square profile pictures being squished" 2022-05-24 19:50:37 +02:00
Fallenbagel
046ae932ec Merge pull request #104 from sambartik/fix-profilepic
Fix non-square profile pictures being squished
2022-05-24 07:02:17 +05:00
Samuel Bartík
1570a8715a Fix non-square profile pictures being squished 2022-05-23 15:36:00 +02:00
Danshil Kokil Mungur
90095bb185 feat(manage slideover): show more request override details (#2772)
* feat(manage slideover): show the language profile if request is for a show

* feat(manage slideover): show name of profiles instead of id
2022-05-23 09:49:35 +09:00
Mohamed jinas
2bfdf02c79 feat: conditional media server name 2022-05-22 22:12:05 +05:00
Brandon Cohen
4f972be858 fix(recommendations): fixed recommendations page causing infinite network requests to tmdb api
TMDB API would only return 40 results and the recommendations page expected more. This would cause
an infinite amount of network requests. A limit was implemented to specifically to solve this
problem
2022-05-21 07:35:02 +05:00
Danshil Kokil Mungur
b07f7032ad fix(search): use correct param to filter movies by year 2022-05-21 07:31:59 +05:00
Danshil Kokil Mungur
af23a257d5 feat(api): add issue counts endpoint 2022-05-21 07:29:16 +05:00
Danshil Kokil Mungur
dec4062cdc fix: don't show 0 playcount in slideover 2022-05-21 07:25:33 +05:00
Gian Marco Cinalli
e42153c599 docs: update fail2ban documentation to handle logging changes 2022-05-21 07:24:32 +05:00
TheCatLady
d22bc09652 feat: add Paramount+ to network slider 2022-05-21 07:22:32 +05:00
Brandon Cohen
9ded45fef8 fix: manual browser refresh would redirect to home on search page
If you used the search feature and tried to manual refresh or share the link, it would reset the
query. Taking you back to the home page.
2022-05-21 07:17:02 +05:00
Brandon Cohen
14519ef555 fix(recommendations): only load more titles if there can be more than 40 (#2749)
* fix: fixed recommendations page causing infinite network requests to tmdb api

TMDB API would only return 40 results and the recommendations page expected more. This would cause
an infinite amount of network requests. I set a limit specifically for this solving the problem.

fix #2710
2022-05-13 23:15:53 +04:00
Danshil Kokil Mungur
1054b4e2d7 fix(search): use correct param to filter movies by year (#2727) 2022-04-30 23:31:02 +09:00
Danshil Kokil Mungur
e4039d09c0 feat(api): add issue counts endpoint (#2713) 2022-04-25 23:06:36 +00:00
Danshil Kokil Mungur
29be659512 fix(ui): don't show 0 playcount in slideover (#2714) 2022-04-26 07:53:33 +09:00
Gian Marco Cinalli
475314c87b docs: update fail2ban documentation to handle logging changes (#2707) [skip ci] 2022-04-25 16:13:41 +00:00
TheCatLady
1d00229a48 feat(discover): add Paramount+ to network slider (#2608) 2022-04-25 13:09:22 +00:00
Brandon Cohen
b2878390b4 fix: manual browser refresh would redirect to home on search page (#2692)
If you used the search feature and tried to manual refresh or share the link, it would reset the
query. Taking you back to the home page.

fix #2683
2022-04-25 14:47:38 +04:00
75 changed files with 969 additions and 321 deletions

View File

@@ -0,0 +1,39 @@
name: 'create docker image on pull request and push to private registery'
on:
pull_request:
branches:
- develop
workflow_dispatch:
jobs:
build-image:
runs-on: self-hosted
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to private registery
uses: docker/login-action@v2.0.0
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}'
cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache'
cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max'

View File

@@ -5,7 +5,7 @@ on: workflow_dispatch
jobs:
semantic-release:
name: Tag and release latest version
runs-on: ubuntu-20.04
runs-on: self-hosted
env:
HUSKY: 0
steps:
@@ -26,6 +26,8 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn
- name: Release
@@ -39,7 +41,7 @@ jobs:
name: Send Discord Notification
needs: semantic-release
if: always()
runs-on: ubuntu-20.04
runs-on: self-hosted
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v2

3
.gitignore vendored
View File

@@ -53,3 +53,6 @@ config/db/db.sqlite3-journal
# VS Code
.vscode/launch.json
# Webstorm
.idea

View File

@@ -1,3 +1,57 @@
# [1.1.1](https://github.com/fallenbagel/jellyseerr/compare/v1.1.0...v1.1.1) (2022-06-20)
### Bug Fixes
* conditional media server name for 4k url to add emby to tvdetails ([ddd773c](https://github.com/fallenbagel/jellyseerr/commit/ddd773c03ff61654490644dec21f406d03374b3d))
* don't show 0 playcount in slideover ([dec4062](https://github.com/fallenbagel/jellyseerr/commit/dec4062cdcecbe297f72364ede6a000b863117f4))
* fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings ([94ade93](https://github.com/fallenbagel/jellyseerr/commit/94ade93e16f02b372dafd2765bea475117431975))
* fixes jellyfin forgot password and adds emby support to the forgot password link ([0259975](https://github.com/fallenbagel/jellyseerr/commit/02599754026e6a66662f753bb6b6117dfabb5f9a)), closes [#99](https://github.com/fallenbagel/jellyseerr/issues/99)
* hide plex guid cache settings from ui when running in jellyfin/emby mode ([7450138](https://github.com/fallenbagel/jellyseerr/commit/7450138ac12640797952c1a2d5e1e111d17a11e1))
* **import all:** fis for import all ([29478fc](https://github.com/fallenbagel/jellyseerr/commit/29478fc19534589db37499f1cdcc21ea4d389a74))
* **jellyfin:** ignore additional items with virtual location type ([c811548](https://github.com/fallenbagel/jellyseerr/commit/c81154800fd7dc48fe890f4dd57ff33cbab973bb))
* **jellyfinimportmodal:** fix for importing all jellyfin users ([a483ca9](https://github.com/fallenbagel/jellyseerr/commit/a483ca9837e12e2385d0e2407e52d6c64ae435e2))
* **jellyfin:** sync errors ([d1dbd6e](https://github.com/fallenbagel/jellyseerr/commit/d1dbd6e3b9b1134e06150fc5eb21f729f64c0955))
* manual browser refresh would redirect to home on search page ([9ded45f](https://github.com/fallenbagel/jellyseerr/commit/9ded45fef80b4a7e0be237fbe0301629f862fff9))
* manual browser refresh would redirect to home on search page ([#2692](https://github.com/fallenbagel/jellyseerr/issues/2692)) ([b287839](https://github.com/fallenbagel/jellyseerr/commit/b2878390b486e338151f26a2354711147012f88e)), closes [#2683](https://github.com/fallenbagel/jellyseerr/issues/2683)
* only show mediaserver settings for current active mediaserver ([739f5f9](https://github.com/fallenbagel/jellyseerr/commit/739f5f9c9ade8a1680bcb374f6c9e919a9e1426c))
* **recommendations:** fixed recommendations page causing infinite network requests to tmdb api ([4f972be](https://github.com/fallenbagel/jellyseerr/commit/4f972be8584e48f544268aef9d1d05769ba2e38e))
* **recommendations:** only load more titles if there can be more than 40 ([#2749](https://github.com/fallenbagel/jellyseerr/issues/2749)) ([14519ef](https://github.com/fallenbagel/jellyseerr/commit/14519ef5559038b0d9d037a2bdc5d98e63c9db6f)), closes [#2710](https://github.com/fallenbagel/jellyseerr/issues/2710)
* remove internal Overseerr sponsor link, this is remaining on the main github page instead ([4b7bdd3](https://github.com/fallenbagel/jellyseerr/commit/4b7bdd3d7d608fe0bf52f494766fd7c40bede859))
* **scan:** ignore virtual seasons ([6574e18](https://github.com/fallenbagel/jellyseerr/commit/6574e18516201bc11b5f0c422bf6b432c722e067)), closes [#119](https://github.com/fallenbagel/jellyseerr/issues/119)
* **search:** use correct param to filter movies by year ([b07f703](https://github.com/fallenbagel/jellyseerr/commit/b07f7032ad89ccb359f3a6a4f4508de6b59ec393))
* **search:** use correct param to filter movies by year ([#2727](https://github.com/fallenbagel/jellyseerr/issues/2727)) ([1054b4e](https://github.com/fallenbagel/jellyseerr/commit/1054b4e2d7262d841fa83cde624f1138ad7bd23a))
* **setup&login:** fix a description error in the manual scan in setup and add emby to login page ([8810c20](https://github.com/fallenbagel/jellyseerr/commit/8810c20fc18a55c2f6768ddc40830a8494946072))
* **ui:** don't show 0 playcount in slideover ([#2714](https://github.com/fallenbagel/jellyseerr/issues/2714)) ([29be659](https://github.com/fallenbagel/jellyseerr/commit/29be6595125017700eccb34d33a0e852f23c97ba))
* **ui:** fix Avatar being broken when setup using internal ip ([01e81a7](https://github.com/fallenbagel/jellyseerr/commit/01e81a73a3ae3c4692d0b9b68dc27fe1a54b1a1d)), closes [#110](https://github.com/fallenbagel/jellyseerr/issues/110)
* **ui:** fix translation errors for all locales in the import plex user button ([0fb5803](https://github.com/fallenbagel/jellyseerr/commit/0fb5803eb9a7589141a63e13df9a8aa8ea4cebf2))
* **ui:** fix ui elements not reflecting the env variable ([722dda5](https://github.com/fallenbagel/jellyseerr/commit/722dda585631be365a2fb400b62dbc201f2b80de))
* **ui:** fixed translation issue where it showed as import {mediaServerName} user ([819190c](https://github.com/fallenbagel/jellyseerr/commit/819190ce98720d8d66a07c98a4f12e3c8cdcac94))
* **ui:** rectangular avatars getting stretched ([#2782](https://github.com/fallenbagel/jellyseerr/issues/2782)) ([db05172](https://github.com/fallenbagel/jellyseerr/commit/db05172d8b924a591ece4fae72d076eb59ee5f82))
* **ui:** replaced {mediaServerName} in the plex variable in NL locale ([d417fca](https://github.com/fallenbagel/jellyseerr/commit/d417fcafa1e38c6d56ed8360ae451e8b8ff82a8d))
### Features
* add Paramount+ to network slider ([d22bc09](https://github.com/fallenbagel/jellyseerr/commit/d22bc09652e5d4e703fca6838d06e4908432fe06))
* **api:** add issue counts endpoint ([af23a25](https://github.com/fallenbagel/jellyseerr/commit/af23a257d5795b5c3930cd3884a84a2e2eeeb1dc))
* **api:** add issue counts endpoint ([#2713](https://github.com/fallenbagel/jellyseerr/issues/2713)) ([e4039d0](https://github.com/fallenbagel/jellyseerr/commit/e4039d09c0380d80f03c7a00b51a150f88c02cca))
* conditional media server name ([2bfdf02](https://github.com/fallenbagel/jellyseerr/commit/2bfdf02c7942762bd9f5201459b1a9ad6003b9a6))
* conditional media server name to add emby to tvdetails ([e75b71b](https://github.com/fallenbagel/jellyseerr/commit/e75b71b8168b4a661971b809c88f9910c4206545))
* conditional media server name to add emby to tvdetails ([ff3e3ce](https://github.com/fallenbagel/jellyseerr/commit/ff3e3ce841f0676713242d0c8e3a977ef65530d8))
* **discover:** add Paramount+ to network slider ([#2608](https://github.com/fallenbagel/jellyseerr/issues/2608)) ([1d00229](https://github.com/fallenbagel/jellyseerr/commit/1d00229a485bb2b376e9f63b52c70c7719f5f023))
* email ([a8bc0c0](https://github.com/fallenbagel/jellyseerr/commit/a8bc0c068b305710a224fa56a3725cc7e0758eb7)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
* **email validation:** email requirement and validation + better importer ([d835336](https://github.com/fallenbagel/jellyseerr/commit/d835336d330abfef5b15bc9febcb748a8154c7df))
* **manage slideover:** show more request override details ([#2772](https://github.com/fallenbagel/jellyseerr/issues/2772)) ([90095bb](https://github.com/fallenbagel/jellyseerr/commit/90095bb18548dfd663a78df1908c40dbf2f99faf))
* **uesrprofile:** email requirement and validation ([543859e](https://github.com/fallenbagel/jellyseerr/commit/543859e6f3b3a8cd4c61499a74bda610d3217626))
* **ui:** add emby as a mediaServerType to the import user button ([6a6bfe0](https://github.com/fallenbagel/jellyseerr/commit/6a6bfe0c6875a1d8ccb1a6fdc409f595202ef38e))
* **ui:** add emby user badge to the user list and fix local user badge ([410b536](https://github.com/fallenbagel/jellyseerr/commit/410b536c9474806ab9f7f5f097cedfafde1fbf67))
* **ui:** add emby user badge to the userProfile ([b9546e6](https://github.com/fallenbagel/jellyseerr/commit/b9546e6daa8583c60fac7961447a13715bbc7f6b))
* **ui:** conditional media server name to add emby to issuedetails play on button ([377a4fd](https://github.com/fallenbagel/jellyseerr/commit/377a4fd85b7194afb48a8ba9bfa4ce4ccf996be8))
* **ui:** conditional media server name to add emby to moviedetails ([14d2937](https://github.com/fallenbagel/jellyseerr/commit/14d293799bb37f45449c201ab03638af257623be))
* **user email setting:** added field to save user email ([30c48f1](https://github.com/fallenbagel/jellyseerr/commit/30c48f16ca0a74e7551b533bd75bc43304f946b1)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
* **user settings:** added email field to user profiel settings ([b22f20b](https://github.com/fallenbagel/jellyseerr/commit/b22f20b6fa5f68398850ccbf9b6e1cc233b3c8f4)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
# [1.1.0](https://github.com/fallenbagel/jellyseerr/compare/v1.0.2...v1.1.0) (2022-05-21)

View File

@@ -32,6 +32,18 @@ With more features on the way! Check out our [issue tracker](https://github.com/
Check out our dockerhub for instructions on how to install and run Jellyseerr:
https://hub.docker.com/r/fallenbagel/jellyseerr
### Launching Jellyseerr manually:
```bash
yarn install
yarn run build
yarn start
```
### Packages:
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
## Preview
<img src="./public/preview.jpg">

View File

@@ -8,7 +8,7 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you
```
[Definition]
failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"<HOST>"
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
```
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.

View File

@@ -2,6 +2,10 @@ module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',
},
publicRuntimeConfig: {
// Will be available on both server and client
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
},
images: {
domains: ['image.tmdb.org'],
},

View File

@@ -5815,6 +5815,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Issue'
/issue/count:
get:
summary: Gets issue counts
description: |
Returns the number of open and closed issues, as well as the number of issues of each type.
tags:
- issue
responses:
'200':
description: Issue counts returned
content:
application/json:
schema:
type: object
properties:
total:
type: number
video:
type: number
audio:
type: number
subtitles:
type: number
others:
type: number
open:
type: number
closed:
type: number
/issue/{issueId}:
get:
summary: Get issue

View File

@@ -1,6 +1,6 @@
{
"name": "jellyseerr",
"version": "1.1.0",
"version": "1.1.1",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
@@ -37,6 +37,7 @@
"country-flag-icons": "^1.4.21",
"csurf": "^1.11.0",
"email-templates": "^8.0.10",
"email-validator": "^2.0.4",
"express": "^4.17.3",
"express-openapi-validator": "^4.13.6",
"express-rate-limit": "^6.3.0",
@@ -84,6 +85,7 @@
"@babel/cli": "^7.17.6",
"@commitlint/cli": "^16.2.1",
"@commitlint/config-conventional": "^16.2.1",
"@next/eslint-plugin-next": "^12.1.6",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/exec": "^6.0.3",

View File

@@ -31,6 +31,7 @@ export interface JellyfinLibraryItem {
Id: string;
HasSubtitles: boolean;
Type: 'Movie' | 'Episode' | 'Season' | 'Series';
LocationType: 'FileSystem' | 'Offline' | 'Remote' | 'Virtual';
SeriesName?: string;
SeriesId?: string;
SeasonId?: string;
@@ -205,7 +206,9 @@ class JellyfinAPI {
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
@@ -251,7 +254,9 @@ class JellyfinAPI {
try {
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
@@ -270,7 +275,9 @@ class JellyfinAPI {
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
);
return contents.data.Items;
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
} catch (e) {
logger.error(
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,

View File

@@ -129,7 +129,13 @@ class TheMovieDb extends ExternalAPI {
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
try {
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
params: { query, page, include_adult: includeAdult, language, year },
params: {
query,
page,
include_adult: includeAdult,
language,
primary_release_year: year,
},
});
return data;

View File

@@ -137,6 +137,8 @@ export class User {
@UpdateDateColumn()
public updatedAt: Date;
public warnings: string[] = [];
constructor(init?: Partial<User>) {
Object.assign(this, init);
}

View File

@@ -2,6 +2,7 @@ import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
email?: string;
discordId?: string;
locale?: string;
region?: string;

View File

@@ -13,6 +13,7 @@ import {
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import * as EmailValidator from 'email-validator';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
@@ -215,14 +216,23 @@ class EmailAgent
this.getSettings(),
payload.notifyUser.settings?.pgpKey
);
await email.send(
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
if (EmailValidator.validate(payload.notifyUser.email)) {
await email.send(
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
} else {
logger.warn('Invalid email address provided for user', {
label: 'Notifications',
recipient: payload.notifyUser.displayName,
type: Notification[type],
subject: payload.subject,
});
}
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',
@@ -268,9 +278,18 @@ class EmailAgent
this.getSettings(),
user.settings?.pgpKey
);
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
if (EmailValidator.validate(user.email)) {
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
} else {
logger.warn('Invalid email address provided for user', {
label: 'Notifications',
recipient: user.displayName,
type: Notification[type],
subject: payload.subject,
});
}
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',

View File

@@ -134,6 +134,7 @@ interface FullPublicSettings extends PublicSettings {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
userEmailRequired: boolean;
newPlexLogin: boolean;
}
@@ -159,6 +160,7 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
export interface NotificationAgentEmail extends NotificationAgentConfig {
options: {
userEmailRequired: boolean;
emailFrom: string;
smtpHost: string;
smtpPort: number;
@@ -335,6 +337,7 @@ class Settings {
email: {
enabled: false,
options: {
userEmailRequired: false,
emailFrom: '',
smtpHost: '',
smtpPort: 587,
@@ -342,7 +345,7 @@ class Settings {
ignoreTls: false,
requireTls: false,
allowSelfSigned: false,
senderName: 'Overseerr',
senderName: 'Jellyseerr',
},
},
discord: {
@@ -529,6 +532,8 @@ class Settings {
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
locale: this.data.main.locale,
emailEnabled: this.data.notifications.agents.email.enabled,
userEmailRequired:
this.data.notifications.agents.email.options.userEmailRequired,
newPlexLogin: this.data.main.newPlexLogin,
};
}

View File

@@ -9,6 +9,7 @@ import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { isAuthenticated } from '../middleware/auth';
import * as EmailValidator from 'email-validator';
const authRoutes = Router();
@@ -24,6 +25,16 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
where: { id: req.user.id },
});
// check if email is required in settings and if user has an valid email
const settings = await getSettings();
if (
settings.notifications.agents.email.options.userEmailRequired &&
!EmailValidator.validate(user.email)
) {
user.warnings.push('userEmailRequired');
logger.warn(`User ${user.username} has no valid email address`);
}
return res.status(200).json(user);
});
@@ -70,6 +81,9 @@ authRoutes.post('/plex', async (req, res, next) => {
userType: UserType.PLEX,
});
settings.main.mediaServerType = MediaServerType.PLEX;
settings.save();
await userRepository.save(user);
} else {
const mainUser = await userRepository.findOneOrFail({
@@ -196,10 +210,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
settings.jellyfin.hostname !== ''
) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username || !body.password) {
return res
.status(500)
.json({ error: 'You must provide an username and a password' });
} else if (!body.username) {
return res.status(500).json({ error: 'You must provide an username' });
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
return res
.status(500)
@@ -213,6 +225,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
settings.jellyfin.hostname !== ''
? settings.jellyfin.hostname
: body.hostname;
const { externalHostname } = getSettings().jellyfin;
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
let user = await userRepository.findOne({
@@ -229,6 +242,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
}
// First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
const account = await jellyfinserver.login(body.username, body.password);
// Next let's see if the user already exists
@@ -244,7 +261,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
user.avatar = '/os_logo_square.png';
}
@@ -290,7 +307,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
@@ -319,7 +336,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
@@ -327,7 +344,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
const passedExplicitPassword =
body.password && body.password.length > 0;
if (passedExplicitPassword) {
await user.setPassword(body.password);
await user.setPassword(body.password ?? '');
}
await userRepository.save(user);
}

View File

@@ -1,6 +1,6 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { IssueStatus } from '../constants/issue';
import { IssueStatus, IssueType } from '../constants/issue';
import Issue from '../entity/Issue';
import IssueComment from '../entity/IssueComment';
import Media from '../entity/Media';
@@ -146,6 +146,68 @@ issueRoutes.post<
}
);
issueRoutes.get('/count', async (req, res, next) => {
const issueRepository = getRepository(Issue);
try {
const query = issueRepository.createQueryBuilder('issue');
const totalCount = await query.getCount();
const videoCount = await query
.where('issue.issueType = :issueType', {
issueType: IssueType.VIDEO,
})
.getCount();
const audioCount = await query
.where('issue.issueType = :issueType', {
issueType: IssueType.AUDIO,
})
.getCount();
const subtitlesCount = await query
.where('issue.issueType = :issueType', {
issueType: IssueType.SUBTITLES,
})
.getCount();
const othersCount = await query
.where('issue.issueType = :issueType', {
issueType: IssueType.OTHER,
})
.getCount();
const openCount = await query
.where('issue.status = :issueStatus', {
issueStatus: IssueStatus.OPEN,
})
.getCount();
const closedCount = await query
.where('issue.status = :issueStatus', {
issueStatus: IssueStatus.RESOLVED,
})
.getCount();
return res.status(200).json({
total: totalCount,
video: videoCount,
audio: audioCount,
subtitles: subtitlesCount,
others: othersCount,
open: openCount,
closed: closedCount,
});
} catch (e) {
logger.debug('Something went wrong retrieving issue counts.', {
label: 'API',
errorMessage: e.message,
});
next({ status: 500, message: 'Unable to retrieve issue counts.' });
}
});
issueRoutes.get<{ issueId: string }>(
'/:issueId',
isAuthenticated(

View File

@@ -303,6 +303,11 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
settingsRoutes.get('/jellyfin/users', async (req, res) => {
const settings = getSettings();
const { hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
@@ -321,7 +326,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
username: user.Name,
id: user.Id,
thumb: user.PrimaryImageTag
? `${settings.jellyfin.hostname}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
email: user.Name,
}));

View File

@@ -492,56 +492,46 @@ router.post(
);
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsersResponse = await jellyfinClient.getUsers();
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
const createdUsers: User[] = [];
for (const account of jellyfinUsersResponse.users) {
if (account.Name) {
const user = await userRepository
.createQueryBuilder('user')
.where('user.jellyfinUserId = :id', { id: account.Id })
.orWhere('user.email = :email', {
email: account.Name,
})
.getOne();
const { hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
const avatar = account.PrimaryImageTag
? `${settings.jellyfin.hostname}/Users/${account.Id}/Images/Primary/?tag=${account.PrimaryImageTag}&quality=90`
: '/os_logo_square.png';
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsers = await jellyfinClient.getUsers();
if (user) {
// Update the user's avatar with their Jellyfin thumbnail, in case it changed
user.avatar = avatar;
user.email = account.Name;
user.jellyfinUsername = account.Name;
for (const jellyfinUserId of body.jellyfinUserIds) {
const jellyfinUser = jellyfinUsers.users.find(
(user) => user.Id === jellyfinUserId
);
// In case the user was previously a local account
if (user.userType === UserType.LOCAL) {
user.userType = UserType.JELLYFIN;
user.jellyfinUserId = account.Id;
}
await userRepository.save(user);
} else if (!body || body.jellyfinUserIds.includes(account.Id)) {
// logger.error('CREATED USER', {
// label: 'API',
// });
const user = await userRepository.findOne({
select: ['id', 'jellyfinUserId'],
where: { jellyfinUserId: jellyfinUserId },
});
const newUser = new User({
jellyfinUsername: account.Name,
jellyfinUserId: account.Id,
jellyfinDeviceId: Buffer.from(
`BOT_overseerr_${account.Name ?? ''}`
).toString('base64'),
email: account.Name,
permissions: settings.main.defaultPermissions,
avatar,
userType: UserType.JELLYFIN,
});
await userRepository.save(newUser);
createdUsers.push(newUser);
}
if (!user) {
const newUser = new User({
jellyfinUsername: jellyfinUser?.Name,
jellyfinUserId: jellyfinUser?.Id,
jellyfinDeviceId: Buffer.from(
`BOT_jellyseerr_${jellyfinUser?.Name ?? ''}`
).toString('base64'),
email: jellyfinUser?.Name,
permissions: settings.main.defaultPermissions,
avatar: jellyfinUser?.PrimaryImageTag
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
await userRepository.save(newUser);
createdUsers.push(newUser);
}
}
return res.status(201).json(User.filterMany(createdUsers));
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -51,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
return res.status(200).json({
username: user.username,
email: user.email,
discordId: user.settings?.discordId,
locale: user.settings?.locale,
region: user.settings?.region,
@@ -120,6 +121,7 @@ userSettingsRoutes.post<
user.settings.locale = req.body.locale;
user.settings.region = req.body.region;
user.settings.originalLanguage = req.body.originalLanguage;
user.email = req.body.email ?? user.email;
}
await userRepository.save(user);
@@ -130,6 +132,7 @@ userSettingsRoutes.post<
locale: user.settings.locale,
region: user.settings.region,
originalLanguage: user.settings.originalLanguage,
email: user.email,
});
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -110,6 +110,12 @@ const networks: Network[] = [
'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png',
url: '/discover/tv/network/16',
},
{
name: 'Paramount+',
image:
'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/fi83B1oztoS47xxcemFdPMhIzK.png',
url: '/discover/tv/network/4330',
},
{
name: 'BBC One',
image:

View File

@@ -90,7 +90,7 @@ const IssueComment: React.FC<IssueCommentProps> = ({
<img
src={comment.user.avatar}
alt=""
className="h-10 w-10 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -35,6 +35,7 @@ import IssueComment from './IssueComment';
import IssueDescription from './IssueDescription';
import { MediaServerType } from '../../../server/constants/server';
import useSettings from '../../hooks/useSettings';
import getConfig from 'next/config';
const messages = defineMessages({
openedby: '#{issueId} opened {relativeTime} by {username}',
@@ -99,6 +100,7 @@ const IssueDetails: React.FC = () => {
(opt) => opt.issueType === issueData?.issueType
);
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -267,7 +269,7 @@ const IssueDetails: React.FC = () => {
>
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5">
<img
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
src={issueData.createdBy.avatar}
alt=""
/>
@@ -366,13 +368,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{intl.formatMessage(messages.playonplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.playonplex, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -407,13 +414,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{intl.formatMessage(messages.play4konplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -618,13 +630,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{intl.formatMessage(messages.playonplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.playonplex, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -659,13 +676,18 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{intl.formatMessage(messages.play4konplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
)}

View File

@@ -228,7 +228,7 @@ const IssueItem: React.FC<IssueItemProps> = ({ issue }) => {
<img
src={issue.createdBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{issue.createdBy.displayName}

View File

@@ -14,6 +14,7 @@ import useClickOutside from '../../../hooks/useClickOutside';
import { Permission, useUser } from '../../../hooks/useUser';
import Transition from '../../Transition';
import VersionStatus from '../VersionStatus';
import UserWarnings from '../UserWarnings';
const messages = defineMessages({
dashboard: 'Discover',
@@ -177,6 +178,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
);
})}
</nav>
<div className="px-2">
<UserWarnings onClick={() => setClosed()} />
</div>
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus onClick={() => setClosed()} />
@@ -236,6 +241,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
);
})}
</nav>
<div className="px-2">
<UserWarnings />
</div>
{hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus />

View File

@@ -40,7 +40,7 @@ const UserDropdown: React.FC = () => {
onClick={() => setDropdownOpen(true)}
>
<img
className="h-8 w-8 rounded-full sm:h-10 sm:w-10"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar}
alt=""
/>

View File

@@ -0,0 +1,66 @@
import React from 'react';
import Link from 'next/link';
import { ExclamationIcon } from '@heroicons/react/outline';
import { defineMessages, useIntl } from 'react-intl';
import { useUser } from '../../../hooks/useUser';
const messages = defineMessages({
emailRequired: 'An email address is required.',
emailInvalid: 'Email address is invalid.',
passwordRequired: 'A password is required.',
});
interface UserWarningsProps {
onClick?: () => void;
}
const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
const intl = useIntl();
const { user } = useUser();
if (!user) {
return null;
}
let res = null;
//check if a user has warnings
if (user.warnings.length > 0) {
user.warnings.forEach((warning) => {
let link = '';
let warningText = '';
let warningTitle = '';
switch (warning) {
case 'userEmailRequired':
link = '/profile/settings/';
warningTitle = 'Profile is incomplete';
warningText = intl.formatMessage(messages.emailRequired);
}
res = (
<Link href={link}>
<a
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' && onClick) {
onClick();
}
}}
role="button"
tabIndex={0}
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
>
<ExclamationIcon className="h-6 w-6" />
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
<span className="font-bold">{warningTitle}</span>
<span className="truncate">{warningText}</span>
</div>
</a>
</Link>
);
});
}
return res;
};
export default UserWarnings;

View File

@@ -50,6 +50,7 @@ const Layout: React.FC = ({ children }) => {
<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)} />
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">

View File

@@ -1,19 +1,19 @@
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup';
import useSettings from '../../hooks/useSettings';
import Button from '../Common/Button';
import getConfig from 'next/config';
const messages = defineMessages({
username: 'Username',
password: 'Password',
host: 'Jellyfin URL',
host: '{mediaServerName} URL',
email: 'Email',
validationhostrequired: 'Jellyfin URL required',
validationhostrequired: '{mediaServerName} URL required',
validationhostformat: 'Valid URL required',
validationemailrequired: 'Email required',
validationemailformat: 'Valid email required',
@@ -40,6 +40,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
const toasts = useToasts();
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (initial) {
const LoginSchema = Yup.object().shape({
@@ -48,16 +49,19 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
intl.formatMessage(messages.validationhostformat)
)
.required(intl.formatMessage(messages.validationhostrequired)),
.required(
intl.formatMessage(messages.validationhostrequired, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})
),
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string().required(
intl.formatMessage(messages.validationpasswordrequired)
),
password: Yup.string(),
});
return (
<Formik
@@ -97,7 +101,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
<Form>
<div className="sm:border-t sm:border-gray-800">
<label htmlFor="host" className="text-label">
{intl.formatMessage(messages.host)}
{intl.formatMessage(messages.host, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
</label>
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
<div className="flex rounded-md shadow-sm">
@@ -105,7 +114,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
id="host"
name="host"
type="text"
placeholder={intl.formatMessage(messages.host)}
placeholder={intl.formatMessage(messages.host, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
/>
</div>
{errors.host && touched.host && (
@@ -185,9 +199,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired)
),
password: Yup.string().required(
intl.formatMessage(messages.validationpasswordrequired)
),
password: Yup.string(),
});
return (
<div>
@@ -266,8 +278,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
as="a"
buttonType="ghost"
href={
settings.currentSettings.jellyfinHost +
'/web/#!/forgotpassword.html'
process.env.JELLYFIN_TYPE == 'emby'
? settings.currentSettings.jellyfinHost +
'/web/index.html#!/startup/forgotpassword.html'
: settings.currentSettings.jellyfinHost +
'/web/index.html#!/forgotpassword.html'
}
>
{intl.formatMessage(messages.forgotpassword)}

View File

@@ -15,12 +15,13 @@ import PlexLoginButton from '../PlexLoginButton';
import Transition from '../Transition';
import JellyfinLogin from './JellyfinLogin';
import LocalLogin from './LocalLogin';
import getConfig from 'next/config';
const messages = defineMessages({
signin: 'Sign In',
signinheader: 'Sign in to continue',
signinwithplex: 'Use your Plex account',
signinwithjellyfin: 'Use your Jellyfin account',
signinwithjellyfin: 'Use your {mediaServerName} account',
signinwithoverseerr: 'Use your {applicationTitle} account',
});
@@ -32,6 +33,7 @@ const Login: React.FC = () => {
const { user, revalidate } = useUser();
const router = useRouter();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
// 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
@@ -133,7 +135,12 @@ const Login: React.FC = () => {
{settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin)}
: intl.formatMessage(messages.signinwithjellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8">

View File

@@ -210,7 +210,7 @@ const ManageSlideOver: React.FC<
{hasPermission(Permission.ADMIN) &&
(data.mediaInfo?.serviceUrl ||
data.mediaInfo?.tautulliUrl ||
watchData?.data?.playCount) && (
!!watchData?.data?.playCount) && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalMedia)}
@@ -272,7 +272,7 @@ const ManageSlideOver: React.FC<
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>
@@ -325,7 +325,7 @@ const ManageSlideOver: React.FC<
{hasPermission(Permission.ADMIN) &&
(data.mediaInfo?.serviceUrl4k ||
data.mediaInfo?.tautulliUrl4k ||
watchData?.data4k?.playCount) && (
!!watchData?.data4k?.playCount) && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalMedia4k)}
@@ -387,7 +387,7 @@ const ManageSlideOver: React.FC<
<img
src={user.avatar}
alt={user.displayName}
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -48,6 +48,7 @@ import PersonCard from '../PersonCard';
import RequestButton from '../RequestButton';
import Slider from '../Slider';
import StatusBadge from '../StatusBadge';
import getConfig from 'next/config';
const messages = defineMessages({
originaltitle: 'Original Title',
@@ -95,6 +96,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
const minStudios = 3;
const [showMoreStudios, setShowMoreStudios] = useState(false);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -130,10 +132,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
if (data.mediaInfo?.mediaUrl) {
mediaLinks.push({
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
text: getAvalaibleMediaServerName(),
url: data.mediaInfo?.mediaUrl,
svg: <PlayIcon />,
});
@@ -146,10 +145,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
})
) {
mediaLinks.push({
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
text: getAvalaible4kMediaServerName(),
url: data.mediaInfo?.mediaUrl4k,
svg: <PlayIcon />,
});
@@ -228,6 +224,30 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play, { mediaServerName: 'Plex' });
}
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
}
function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' });
}
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
}
return (
<div
className="media-page"

View File

@@ -26,6 +26,7 @@ const messages = defineMessages({
server: 'Destination Server',
profilechanged: 'Quality Profile',
rootfolder: 'Root Folder',
languageprofile: 'Language Profile',
});
interface RequestBlockProps {
@@ -38,7 +39,8 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const { profile, rootFolder, server } = useRequestOverride(request);
const { profile, rootFolder, server, languageProfile } =
useRequestOverride(request);
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
setIsUpdating(true);
@@ -209,7 +211,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div>
</div>
)}
{(server || profile !== null || rootFolder) && (
{(server || profile || rootFolder || languageProfile) && (
<>
<div className="mt-4 mb-1 text-sm">
{intl.formatMessage(messages.requestoverrides)}
@@ -223,12 +225,12 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<span>{server}</span>
</li>
)}
{profile !== null && (
{profile && (
<li className="flex justify-between px-1 py-2">
<span className="font-bold">
{intl.formatMessage(messages.profilechanged)}
</span>
<span>ID {profile}</span>
<span>{profile}</span>
</li>
)}
{rootFolder && (
@@ -239,6 +241,14 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<span>{rootFolder}</span>
</li>
)}
{languageProfile && (
<li className="flex justify-between px-1 py-2">
<span className="mr-2 font-bold">
{intl.formatMessage(messages.languageprofile)}
</span>
<span>{languageProfile}</span>
</li>
)}
</ul>
</>
)}

View File

@@ -231,7 +231,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm"
className="avatar-sm object-cover"
/>
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}

View File

@@ -336,7 +336,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
<img
src={requestData.requestedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.requestedBy.displayName}
@@ -390,7 +390,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
<img
src={requestData.modifiedBy.avatar}
alt=""
className="avatar-sm ml-1.5"
className="avatar-sm ml-1.5 object-cover"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{requestData.modifiedBy.displayName}

View File

@@ -534,7 +534,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
<img
src={selectedUser.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full"
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
/>
<span className="ml-3 block">
{selectedUser.displayName}
@@ -584,7 +584,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
<img
src={user.avatar}
alt=""
className="h-6 w-6 flex-shrink-0 rounded-full"
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
/>
<span className="ml-3 block flex-shrink-0">
{user.displayName}

View File

@@ -16,6 +16,7 @@ const messages = defineMessages({
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
validationSmtpPortRequired: 'You must provide a valid port number',
agentenabled: 'Enable Agent',
userEmailRequired: 'Require user email',
emailsender: 'Sender Address',
smtpHost: 'SMTP Host',
smtpPort: 'SMTP Port',
@@ -125,6 +126,7 @@ const NotificationsEmail: React.FC = () => {
<Formik
initialValues={{
enabled: data.enabled,
userEmailRequired: data.options.userEmailRequired,
emailFrom: data.options.emailFrom,
smtpHost: data.options.smtpHost,
smtpPort: data.options.smtpPort ?? 587,
@@ -148,6 +150,7 @@ const NotificationsEmail: React.FC = () => {
await axios.post('/api/v1/settings/notifications/email', {
enabled: values.enabled,
options: {
userEmailRequired: values.userEmailRequired,
emailFrom: values.emailFrom,
smtpHost: values.smtpHost,
smtpPort: Number(values.smtpPort),
@@ -241,6 +244,18 @@ const NotificationsEmail: React.FC = () => {
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="userEmailRequired" className="checkbox-label">
{intl.formatMessage(messages.userEmailRequired)}
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="userEmailRequired"
name="userEmailRequired"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}

View File

@@ -188,35 +188,6 @@ const SettingsAbout: React.FC = () => {
</List.Item>
</List>
</div>
<div className="section">
<List title={intl.formatMessage(messages.supportoverseerr)}>
<List.Item
title={`${intl.formatMessage(messages.helppaycoffee)} ☕️`}
>
<a
href="https://github.com/sponsors/sct"
target="_blank"
rel="noreferrer"
className="text-indigo-500 transition duration-300 hover:underline"
>
https://github.com/sponsors/sct
</a>
<Badge className="ml-2">
{intl.formatMessage(messages.preferredmethod)}
</Badge>
</List.Item>
<List.Item title="">
<a
href="https://patreon.com/overseerr"
target="_blank"
rel="noreferrer"
className="text-indigo-500 transition duration-300 hover:underline"
>
https://patreon.com/overseerr
</a>
</List.Item>
</List>
</div>
<div className="section">
<Releases currentVersion={data.version} />
</div>

View File

@@ -12,30 +12,31 @@ import Badge from '../Common/Badge';
import Button from '../Common/Button';
import LoadingSpinner from '../Common/LoadingSpinner';
import LibraryItem from './LibraryItem';
import getConfig from 'next/config';
const messages = defineMessages({
jellyfinsettings: 'Jellyfin Settings',
jellyfinsettings: '{mediaServerName} Settings',
jellyfinsettingsDescription:
'Configure the settings for your Jellyfin server. Jellyfin scans your Jellyfin libraries to see what content is available.',
'Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.',
timeout: 'Timeout',
save: 'Save Changes',
saving: 'Saving…',
jellyfinlibraries: 'Jellyfin Libraries',
jellyfinlibraries: '{mediaServerName} Libraries',
jellyfinlibrariesDescription:
'The libraries Jellyfin scans for titles. Click the button below if no libraries are listed.',
'The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.',
jellyfinSettingsFailure:
'Something went wrong while saving Jellyfin settings.',
jellyfinSettingsSuccess: 'Jellyfin settings saved successfully!',
jellyfinSettings: 'Jellyfin Settings',
'Something went wrong while saving {mediaServerName} settings.',
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
jellyfinSettings: '{mediaServerName} Settings',
jellyfinSettingsDescription:
'Optionally configure an external player endpoint for your jellyfin server that is different to the internal URL used during setup',
'Optionally configure an external player endpoint for your {mediaServerName} server that is different to the internal URL used during setup',
externalUrl: 'External URL',
validationUrl: 'You must provide a valid URL',
syncing: 'Syncing',
syncJellyfin: 'Sync Libraries',
manualscanJellyfin: 'Manual Library Scan',
manualscanDescriptionJellyfin:
"Normally, this will only be run once every 24 hours. Jellyfin will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
"Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!",
notrunning: 'Not Running',
currentlibrary: 'Current Library: {name}',
librariesRemaining: 'Libraries Remaining: {count}',
@@ -80,6 +81,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
);
const intl = useIntl();
const { addToast } = useToasts();
const { publicRuntimeConfig } = getConfig();
const JellyfinSettingsSchema = Yup.object().shape({
jellyfinExternalUrl: Yup.string().matches(
@@ -161,10 +163,22 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<>
<div className="mb-6">
<h3 className="heading">
<FormattedMessage {...messages.jellyfinlibraries} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Jellyfin',
})}
</h3>
<p className="description">
<FormattedMessage {...messages.jellyfinlibrariesDescription} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<div className="section">
@@ -201,7 +215,13 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<FormattedMessage {...messages.manualscanJellyfin} />
</h3>
<p className="description">
<FormattedMessage {...messages.manualscanDescriptionJellyfin} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<div className="section">
@@ -305,10 +325,22 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<>
<div className="mt-10 mb-6">
<h3 className="heading">
{intl.formatMessage(messages.jellyfinSettings)}
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Jellyfin',
})}
</h3>
<p className="description">
{intl.formatMessage(messages.jellyfinSettingsDescription)}
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Jellyfin',
})}
</p>
</div>
<Formik
@@ -322,15 +354,31 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
externalHostname: values.jellyfinExternalUrl,
} as JellyfinSettings);
addToast(intl.formatMessage(messages.jellyfinSettingsSuccess), {
autoDismiss: true,
appearance: 'success',
});
addToast(
intl.formatMessage(messages.jellyfinSettingsSuccess, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'success',
}
);
} catch (e) {
addToast(intl.formatMessage(messages.jellyfinSettingsFailure), {
autoDismiss: true,
appearance: 'error',
});
addToast(
intl.formatMessage(messages.jellyfinSettingsFailure, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
revalidate();
}

View File

@@ -10,9 +10,11 @@ import {
} from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { MediaServerType } from '../../../../server/constants/server';
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
import { JobId } from '../../../../server/lib/settings';
import Spinner from '../../../assets/spinner.svg';
import useSettings from '../../../hooks/useSettings';
import globalMessages from '../../../i18n/globalMessages';
import { formatBytes } from '../../../utils/numberHelpers';
import Badge from '../../Common/Badge';
@@ -102,6 +104,7 @@ const SettingsJobs: React.FC = () => {
const [isSaving, setIsSaving] = useState(false);
const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5);
const [jobScheduleHours, setJobScheduleHours] = useState(1);
const settings = useSettings();
if (!data && !error) {
return <LoadingSpinner />;
@@ -369,22 +372,33 @@ const SettingsJobs: React.FC = () => {
</tr>
</thead>
<Table.TBody>
{cacheData?.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.misses)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.keys)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right">
<Button buttonType="danger" onClick={() => flushCache(cache)}>
<TrashIcon />
<span>{intl.formatMessage(messages.flushcache)}</span>
</Button>
</Table.TD>
</tr>
))}
{cacheData
?.filter(
(cache) =>
!(
settings.currentSettings.mediaServerType !==
MediaServerType.PLEX && cache.id === 'plexguid'
)
)
.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.misses)}</Table.TD>
<Table.TD>{intl.formatNumber(cache.stats.keys)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushCache(cache)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushcache)}</span>
</Button>
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
</div>

View File

@@ -1,5 +1,8 @@
import getConfig from 'next/config';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { MediaServerType } from '../../../server/constants/server';
import useSettings from '../../hooks/useSettings';
import globalMessages from '../../i18n/globalMessages';
import PageTitle from '../Common/PageTitle';
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
@@ -8,7 +11,7 @@ const messages = defineMessages({
menuGeneralSettings: 'General',
menuUsers: 'Users',
menuPlexSettings: 'Plex',
menuJellyfinSettings: 'Jellyfin',
menuJellyfinSettings: '{mediaServerName}',
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
@@ -18,7 +21,8 @@ const messages = defineMessages({
const SettingsLayout: React.FC = ({ children }) => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings();
const settingsRoutes: SettingsRoute[] = [
{
text: intl.formatMessage(messages.menuGeneralSettings),
@@ -30,16 +34,17 @@ const SettingsLayout: React.FC = ({ children }) => {
route: '/settings/users',
regex: /^\/settings\/users/,
},
{
text: intl.formatMessage(messages.menuPlexSettings),
route: '/settings/plex',
regex: /^\/settings\/plex/,
},
{
text: intl.formatMessage(messages.menuJellyfinSettings),
route: '/settings/jellyfin',
regex: /^\/settings\/jellyfin/,
},
settings.currentSettings.mediaServerType === MediaServerType.PLEX
? {
text: intl.formatMessage(messages.menuPlexSettings),
route: '/settings/plex',
regex: /^\/settings\/plex/,
}
: {
text: getAvailableMediaServerName(),
route: '/settings/jellyfin',
regex: /^\/settings\/jellyfin/,
},
{
text: intl.formatMessage(messages.menuServices),
route: '/settings/services',
@@ -76,6 +81,12 @@ const SettingsLayout: React.FC = ({ children }) => {
<div className="mt-10 text-white">{children}</div>
</>
);
function getAvailableMediaServerName() {
return intl.formatMessage(messages.menuJellyfinSettings, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE === 'emby' ? 'Emby' : 'Jellyfin',
});
}
};
export default SettingsLayout;

View File

@@ -14,6 +14,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import PermissionEdit from '../../PermissionEdit';
import QuotaSelector from '../../QuotaSelector';
import getConfig from 'next/config';
const messages = defineMessages({
users: 'Users',
@@ -42,6 +43,7 @@ const SettingsUsers: React.FC = () => {
mutate: revalidate,
} = useSWR<MainSettings>('/api/v1/settings/main');
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -131,16 +133,20 @@ const SettingsUsers: React.FC = () => {
<label htmlFor="newPlexLogin" className="checkbox-label">
{intl.formatMessage(messages.newPlexLogin, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
<span className="label-tip">
{intl.formatMessage(messages.newPlexLoginTip, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}

View File

@@ -5,7 +5,7 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in with your Plex account',
});

View File

@@ -3,14 +3,15 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
import JellyfinLogin from '../Login/JellyfinLogin';
import axios from 'axios';
import { defineMessages, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Accordion from '../Common/Accordion';
import { MediaServerType } from '../../../server/constants/server';
import getConfig from 'next/config';
const messages = defineMessages({
welcome: 'Welcome to Overseerr',
welcome: 'Welcome to Jellyseerr',
signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your Jellyfin account',
signinWithJellyfin: 'Use your {mediaServerName} account',
signinWithPlex: 'Use your Plex account',
});
@@ -24,7 +25,8 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
MediaServerType.NOT_CONFIGURED
);
const { user, revalidate } = useUser();
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to login. If we get a success message, we will
// ask swr to revalidate the user which _shouid_ come back with a valid user.
@@ -91,7 +93,13 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
}`}
onClick={() => handleClick(1)}
>
<FormattedMessage {...messages.signinWithJellyfin} />
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Jellyfin',
})}
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div

View File

@@ -44,6 +44,7 @@ import RequestButton from '../RequestButton';
import RequestModal from '../RequestModal';
import Slider from '../Slider';
import StatusBadge from '../StatusBadge';
import getConfig from 'next/config';
const messages = defineMessages({
firstAirDate: 'First Air Date',
@@ -85,6 +86,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
router.query.manage == '1' ? true : false
);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -124,10 +126,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
mediaLinks.push({
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
text: getAvalaibleMediaServerName(),
url: data.mediaInfo?.mediaUrl,
svg: <PlayIcon />,
});
@@ -141,10 +140,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
mediaLinks.push({
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
text: getAvalaible4kMediaServerName(),
url: data.mediaInfo?.mediaUrl4k,
svg: <PlayIcon />,
});
@@ -228,6 +224,30 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
?.flatrate ?? [];
function getAvalaibleMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play, { mediaServerName: 'Plex' });
}
return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' });
}
function getAvalaible4kMediaServerName() {
if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' });
}
if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) {
return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' });
}
return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' });
}
return (
<div
className="media-page"

View File

@@ -8,6 +8,8 @@ import useSettings from '../../hooks/useSettings';
import globalMessages from '../../i18n/globalMessages';
import Alert from '../Common/Alert';
import Modal from '../Common/Modal';
import getConfig from 'next/config';
import { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
interface JellyfinImportProps {
onCancel?: () => void;
@@ -15,23 +17,25 @@ interface JellyfinImportProps {
}
const messages = defineMessages({
importfromJellyfin: 'Import Jellyfin Users',
importfromJellyfin: 'Import {mediaServerName} Users',
importfromJellyfinerror:
'Something went wrong while importing Jellyfin users.',
'Something went wrong while importing {mediaServerName} users.',
importedfromJellyfin:
'<strong>{userCount}</strong> Jellyfin {userCount, plural, one {user} other {users}} imported successfully!',
'<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!',
user: 'User',
noJellyfinuserstoimport: 'There are no Jellyfin users to import.',
noJellyfinuserstoimport: 'There are no {mediaServerName} users to import.',
newJellyfinsigninenabled:
'The <strong>Enable New Jellyfin Sign-In</strong> setting is currently enabled. Jellyfin users with library access do not need to be imported in order to sign in.',
'The <strong>Enable New {mediaServerName} Sign-In</strong> setting is currently enabled. {mediaServerName} users with library access do not need to be imported in order to sign in.',
});
const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
onCancel,
onComplete,
children,
}) => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const [isImporting, setImporting] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
@@ -47,6 +51,18 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
revalidateOnMount: true,
});
const { data: existingUsers } = useSWR<UserResultsResponse>(
`/api/v1/user?take=${children}`
);
data?.forEach((user, pos) => {
if (
existingUsers?.results.some((data) => data.jellyfinUserId === user.id)
) {
data?.splice(pos, 1);
}
});
const importUsers = async () => {
setImporting(true);
@@ -66,6 +82,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
@@ -77,10 +95,16 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
onComplete();
}
} catch (e) {
addToast(intl.formatMessage(messages.importfromJellyfinerror), {
autoDismiss: true,
appearance: 'error',
});
addToast(
intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'error',
}
);
} finally {
setImporting(false);
}
@@ -110,7 +134,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
return (
<Modal
loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin)}
title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
iconSvg={<InboxInIcon />}
onOk={() => {
importUsers();
@@ -126,6 +153,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
{settings.currentSettings.newPlexLogin && (
<Alert
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
strong: function strong(msg) {
return (
<strong className="font-semibold text-white">{msg}</strong>
@@ -240,7 +271,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
</>
) : (
<Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport)}
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
type="info"
/>
)}

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
@@ -40,7 +41,7 @@ import PlexImportModal from './PlexImportModal';
const messages = defineMessages({
users: 'Users',
userlist: 'User List',
importfromplex: 'Import {mediaServerName} Users',
importfrommediaserver: 'Import {mediaServerName} Users',
user: 'User',
totalrequests: 'Requests',
accounttype: 'Type',
@@ -87,6 +88,7 @@ const UserList: React.FC = () => {
const intl = useIntl();
const router = useRouter();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
@@ -480,7 +482,9 @@ const UserList: React.FC = () => {
setShowImportModal(false);
revalidate();
}}
/>
>
{data.pageInfo.results}
</JellyfinImportModal>
)}
</Transition>
@@ -503,13 +507,18 @@ const UserList: React.FC = () => {
>
<InboxInIcon />
<span>
{intl.formatMessage(messages.importfromplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Jellyfin',
})}
</span>
</Button>
</div>
@@ -596,7 +605,7 @@ const UserList: React.FC = () => {
<Link href={`/users/${user.id}`}>
<a className="h-10 w-10 flex-shrink-0">
<img
className="h-10 w-10 rounded-full"
className="h-10 w-10 rounded-full object-cover"
src={user.avatar}
alt=""
/>
@@ -636,17 +645,23 @@ const UserList: React.FC = () => {
<Badge badgeType="warning">
{intl.formatMessage(messages.plexuser)}
</Badge>
) : (
) : user.userType === UserType.LOCAL ? (
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
mediaServerName: 'Emby',
})}
</Badge>
)}
) : user.userType === UserType.JELLYFIN ? (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
})}
</Badge>
) : null}
</Table.TD>
<Table.TD>
{user.id === 1

View File

@@ -44,7 +44,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
<div className="flex-shrink-0">
<div className="relative">
<img
className="h-24 w-24 rounded-full bg-gray-600 ring-1 ring-gray-700"
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
src={user.avatar}
alt=""
/>

View File

@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { MediaServerType } from '../../../../../server/constants/server';
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
import {
availableLanguages,
@@ -25,11 +24,13 @@ import PageTitle from '../../../Common/PageTitle';
import LanguageSelector from '../../../LanguageSelector';
import QuotaSelector from '../../../QuotaSelector';
import RegionSelector from '../../../RegionSelector';
import getConfig from 'next/config';
const messages = defineMessages({
general: 'General',
generalsettings: 'General Settings',
displayName: 'Display Name',
email: 'Email',
save: 'Save Changes',
saving: 'Saving…',
mediaServerUser: '{mediaServerName} User',
@@ -59,7 +60,7 @@ const messages = defineMessages({
const UserGeneralSettings: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
const { addToast } = useToasts();
const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
@@ -120,8 +121,9 @@ const UserGeneralSettings: React.FC = () => {
</div>
<Formik
initialValues={{
displayName: data?.username,
discordId: data?.discordId,
displayName: data?.username ?? '',
email: data?.email ?? '',
discordId: data?.discordId ?? '',
locale: data?.locale,
region: data?.region,
originalLanguage: data?.originalLanguage,
@@ -136,6 +138,7 @@ const UserGeneralSettings: React.FC = () => {
try {
await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
username: values.displayName,
email: values.email,
discordId: values.discordId,
locale: values.locale,
region: values.region,
@@ -189,19 +192,25 @@ const UserGeneralSettings: React.FC = () => {
<div className="flex max-w-lg items-center">
{user?.userType === UserType.PLEX ? (
<Badge badgeType="warning">
{intl.formatMessage(messages.plexuser)}
</Badge>
) : user?.userType === UserType.LOCAL ? (
<Badge badgeType="default">
{intl.formatMessage(messages.localuser)}
</Badge>
) : (
<Badge badgeType="default">
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
mediaServerName: 'Emby',
})}
</Badge>
)}
) : user?.userType === UserType.JELLYFIN ? (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
})}
</Badge>
) : null}
</div>
</div>
</div>
@@ -239,6 +248,32 @@ const UserGeneralSettings: React.FC = () => {
)}
</div>
</div>
<div className="form-row">
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
{user?.warnings.find((w) => w === 'userEmailRequired') && (
<span className="label-required">*</span>
)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="email"
name="email"
type="text"
placeholder="example@domain.com"
className={
user?.warnings.find((w) => w === 'userEmailRequired')
? 'border-2 border-red-400 focus:border-blue-600'
: ''
}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="discordId" className="text-label">
{intl.formatMessage(messages.discordId)}

View File

@@ -82,7 +82,9 @@ const useDiscover = <T extends BaseMedia, S = Record<string, never>>(
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20);
isEmpty ||
(!!data && (data[data?.length - 1]?.results.length ?? 0) < 20) ||
(!!data && (data[data?.length - 1]?.totalResults ?? 0) < 41);
return {
isLoadingInitialData,

View File

@@ -1,45 +1,61 @@
import useSWR from 'swr';
import { MediaRequest } from '../../server/entity/MediaRequest';
import { ServiceCommonServer } from '../../server/interfaces/api/serviceInterfaces';
import {
ServiceCommonServer,
ServiceCommonServerWithDetails,
} from '../../server/interfaces/api/serviceInterfaces';
interface OverrideStatus {
server: string | null;
profile: number | null;
rootFolder: string | null;
server?: string;
profile?: string;
rootFolder?: string;
languageProfile?: string;
}
const useRequestOverride = (request: MediaRequest): OverrideStatus => {
const { data } = useSWR<ServiceCommonServer[]>(
const { data: allServers } = useSWR<ServiceCommonServer[]>(
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}`
);
if (!data) {
return {
server: null,
profile: null,
rootFolder: null,
};
const { data } = useSWR<ServiceCommonServerWithDetails>(
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}/${
request.serverId
}`
);
if (!data || !allServers) {
return {};
}
const defaultServer = data.find(
const defaultServer = allServers.find(
(server) => server.is4k === request.is4k && server.isDefault
);
const activeServer = data.find((server) => server.id === request.serverId);
const activeServer = allServers.find(
(server) => server.id === request.serverId
);
return {
server:
activeServer && request.serverId !== defaultServer?.id
? activeServer.name
: null,
: undefined,
profile:
defaultServer?.activeProfileId !== request.profileId
? request.profileId
: null,
? data.profiles.find((profile) => profile.id === request.profileId)
?.name
: undefined,
rootFolder:
defaultServer?.activeDirectory !== request.rootFolder
? request.rootFolder
: null,
: undefined,
languageProfile:
request.type === 'tv' &&
defaultServer?.activeLanguageProfileId !== request.languageProfileId
? data.languageProfiles?.find(
(profile) => profile.id === request.languageProfileId
)?.name
: undefined,
};
};

View File

@@ -1,9 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
import type { UrlObject } from 'url';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import useDebouncedState from './useDebouncedState';
import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import type { UrlObject } from 'url';
import type { Nullable } from '../utils/typeHelpers';
import useDebouncedState from './useDebouncedState';
type Url = string | UrlObject;
@@ -48,7 +48,7 @@ const useSearchInput = (): SearchObject => {
* in a new route. If we are, then we only replace the history.
*/
useEffect(() => {
if (debouncedValue !== '') {
if (debouncedValue !== '' && searchOpen) {
if (router.pathname.startsWith('/search')) {
router.replace({
pathname: router.pathname,

View File

@@ -13,6 +13,7 @@ export type { PermissionCheckOptions };
export interface User {
id: number;
warnings: string[];
plexUsername?: string;
username?: string;
displayName: string;

View File

@@ -686,7 +686,8 @@
"components.UserList.nouserstoimport": "No hi ha usuaris nous de Plex a importar.",
"components.UserList.localuser": "Usuari local",
"components.UserList.importfromplexerror": "S'ha produït un error en importar usuaris de Plex.",
"components.UserList.importfromplex": "Importeu usuaris de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importeu usuaris de {mediaServerName}",
"components.UserList.importfromplex": "Importeu usuaris de Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuari} other {usuaris}} de Plex importat correctament!",
"components.TvDetails.watchtrailer": "Veure el tràiler",
"components.TvDetails.viewfullcrew": "Mostreu equip complet",

View File

@@ -850,7 +850,8 @@
"components.UserList.nouserstoimport": "Ingen nye brugere som kan importeres fra Plex.",
"components.UserList.edituser": "Redigér Brugertilladelser",
"components.UserList.email": "Email Adresse",
"components.UserList.importfromplex": "Importér Brugere fra {mediaServerName}",
"components.UserList.importfrommediaserver": "Importér Brugere fra {mediaServerName}",
"components.UserList.importfromplex": "Importér Brugere fra Plex",
"components.UserList.owner": "Ejer",
"components.UserList.password": "Kodeord",
"components.UserList.passwordinfodescription": "Konfigurér en applikations-URL og aktivér emailnotifikationer for at tillade automatisk kodeordsgenerering.",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Neuste",
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
"components.UserList.importfromplexerror": "Beim Importieren von Plex-Benutzern ist etwas schief gelaufen.",
"components.UserList.importfromplex": "{mediaServerName}-Benutzer importieren",
"components.UserList.importfrommediaserver": "{mediaServerName}-Benutzer importieren",
"components.UserList.importfromplex": "Plex-Benutzer importieren",
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien-Crew",
"components.PersonDetails.crewmember": "Crew",

View File

@@ -602,7 +602,8 @@
"components.UserList.localuser": "Τοπικός χρήστης",
"components.UserList.localLoginDisabled": "Η ρύθμιση <strong>Ενεργοποίηση τοπικής σύνδεσης</strong> είναι προς το παρόν απενεργοποιημένη.",
"components.UserList.importfromplexerror": "Κάτι πήγε στραβά κατά την εισαγωγή χρηστών από το Plex.",
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το {mediaServerName}",
"components.UserList.importfrommediaserver": "Εισαγωγή χρηστών από το {mediaServerName}",
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# νέου χρήστη} other {#νέοι χρήστες}} εισήχθησαν απο το Plex επιτυχώς!",
"components.UserList.email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"components.UserList.edituser": "Επεξεργασία δικαιωμάτων χρήστη",

View File

@@ -269,6 +269,7 @@
"components.QuotaSelector.unlimited": "Unlimited",
"components.RegionSelector.regionDefault": "All Regions",
"components.RegionSelector.regionServerDefault": "Default ({region})",
"components.RequestBlock.languageprofile": "Language Profile",
"components.RequestBlock.profilechanged": "Quality Profile",
"components.RequestBlock.requestoverrides": "Request Overrides",
"components.RequestBlock.rootfolder": "Root Folder",
@@ -860,7 +861,8 @@
"components.UserList.edituser": "Edit User Permissions",
"components.UserList.email": "Email Address",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
"components.UserList.importfromplex": "Import {mediaServerName} Users",
"components.UserList.importfrommediaserver": "Import {mediaServerName} Users",
"components.UserList.importfromplex": "Import Plex Users",
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
"components.UserList.localuser": "Local User",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.currentversion": "Actual",
"components.MovieDetails.studio": "{studioCount, plural, one {Estudio} other {Estudios}}",
"components.UserList.importfromplexerror": "Algo salió mal importando usuarios de Plex.",
"components.UserList.importfromplex": "Importar Usuarios de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
"components.UserList.importfromplex": "Importar Usuarios de Plex",
"components.UserList.importedfromplex": "¡{userCount, plural, one {# nuevo usuario} other {# nuevos usuarios}} importado/s de Plex con éxito!",
"components.TvDetails.viewfullcrew": "Ver Equipo Completo",
"components.TvDetails.firstAirDate": "Primera fecha de emisión",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Dernière version",
"components.Settings.SettingsAbout.Releases.currentversion": "Actuelle",
"components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.",
"components.UserList.importfromplex": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfromplex": "Importer les utilisateurs de Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utilisateur} other {utilisateurs}} importé(s) depuis Plex avec succès !",
"components.TvDetails.viewfullcrew": "Voir l'équipe complète de la série",
"components.TvDetails.TvCrew.fullseriescrew": "Équipe complète de la série",

View File

@@ -165,7 +165,8 @@
"components.UserList.password": "Jelszó",
"components.UserList.localuser": "Helyi felhasználó",
"components.UserList.importfromplexerror": "Hiba történt a felhasználók Plex-ről történő importálása közben.",
"components.UserList.importfromplex": "Felhasználók importálása {mediaServerName}-ről",
"components.UserList.importfrommediaserver": "Felhasználók importálása {mediaServerName}-ről",
"components.UserList.importfromplex": "Felhasználók importálása Plex-ről",
"components.UserList.importedfromplex": "{userCount, plural, =0 {Nem lett új} one {# új} other {# új}} felhasználó importálva Plex-ről!",
"components.UserList.email": "E-mail-cím",
"components.UserList.deleteuser": "Felhasználó törlése",

View File

@@ -223,7 +223,8 @@
"components.Settings.SettingsAbout.Releases.latestversion": "Versione più recente",
"components.Settings.SettingsAbout.Releases.currentversion": "Versione attuale",
"components.UserList.importfromplexerror": "Qualcosa è andato storto nell'importare gli utenti Plex.",
"components.UserList.importfromplex": "Importa utenti {mediaServerName}",
"components.UserList.importfrommediaserver": "Importa utenti {mediaServerName}",
"components.UserList.importfromplex": "Importa utenti Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utente} other {utenti}} Plex {userCount, plural, one {importato} other {importati}} correttamente!",
"components.TvDetails.viewfullcrew": "Vedi troupe completa",
"components.TvDetails.TvCrew.fullseriescrew": "Troupe completa serie",

View File

@@ -231,7 +231,8 @@
"components.TvDetails.watchtrailer": "予告編を見る",
"components.MovieDetails.watchtrailer": "予告編を見る",
"components.UserList.importfromplexerror": "Plexからユーザーをインポート中に問題が発生しました。",
"components.UserList.importfromplex": "{mediaServerName}からユーザーをインポート",
"components.UserList.importfrommediaserver": "{mediaServerName}からユーザーをインポート",
"components.UserList.importfromplex": "Plexからユーザーをインポート",
"components.UserList.importedfromplex": "Plex から新ユーザー {userCount} 名をインポートしました。",
"components.TvDetails.viewfullcrew": "フルクルーを表示",
"components.TvDetails.firstAirDate": "初放送日",

View File

@@ -194,7 +194,8 @@
"components.UserList.userssaved": "Brukertillatelsene ble lagret!",
"components.UserList.users": "Brukere",
"components.UserList.importfromplexerror": "Noe gikk galt ved importering av brukere fra Plex.",
"components.UserList.importfromplex": "Importer brukere fra {mediaServerName}",
"components.UserList.importfrommediaserver": "Importer brukere fra {mediaServerName}",
"components.UserList.importfromplex": "Importer brukere fra Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {ny bruker} other {nye brukere}} ble importert fra Plex!",
"components.Settings.menuUsers": "Brukere",
"components.Settings.SettingsUsers.users": "Brukere",

View File

@@ -214,7 +214,8 @@
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
"components.UserList.importfromplexerror": "Er is iets misgegaan bij het importeren van Plex-gebruikers.",
"components.UserList.importfromplex": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfrommediaserver": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfromplex": "Plex-gebruikers importeren",
"components.UserList.deleteuser": "Gebruiker verwijderen",
"components.UserList.deleteconfirm": "Weet je zeker dat je deze gebruiker wilt verwijderen? Al hun bestaande aanvraaggegevens zullen worden verwijderd.",
"components.TvDetails.watchtrailer": "Trailer bekijken",

View File

@@ -962,7 +962,8 @@
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Wyślij po cichu",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Nie udało się zapisać ustawień powiadomień telegram.",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "<FindDiscordIdLink>Wielocyfrowy numer ID</FindDiscordIdLink> powiązany z Twoim kontem użytkownika",
"components.UserList.importfromplex": "Importuj użytkowników {mediaServerName}",
"components.UserList.importfrommediaserver": "Importuj użytkowników {mediaServerName}",
"components.UserList.importfromplex": "Importuj użytkowników Plex",
"i18n.available": "Dostępny",
"components.UserList.sortDisplayName": "Wyświetlana nazwa",
"components.UserList.totalrequests": "Prośby",

View File

@@ -228,7 +228,8 @@
"components.MovieDetails.viewfullcrew": "Ver Equipe Técnica Completa",
"components.MovieDetails.MovieCrew.fullcrew": "Equipe Técnica Completa",
"components.UserList.importfromplexerror": "Algo deu errado ao importar usuários do Plex.",
"components.UserList.importfromplex": "Importar Usuários do {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Usuários do {mediaServerName}",
"components.UserList.importfromplex": "Importar Usuários do Plex",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuário Plex importado} other {usuários Plex importados}} com sucesso!",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Habilitar Agente",
"components.RequestList.RequestItem.failedretry": "Algo deu errado ao retentar fazer a solicitação.",

View File

@@ -199,7 +199,8 @@
"components.UserList.passwordinfodescription": "Configurar um URL de aplicação e ativar as notificações por e-mail para permitir a geração automática de palavra-passe.",
"components.UserList.localuser": "Utilizador Local",
"components.UserList.importfromplexerror": "Ocorreu um erro ao importar utilizadores do Plex.",
"components.UserList.importfromplex": "Importar Utilizadores do {mediaServerName}",
"components.UserList.importfrommediaserver": "Importar Utilizadores do {mediaServerName}",
"components.UserList.importfromplex": "Importar Utilizadores do Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# novo utilizador} other {# novos utilizadores}} importados do Plex com sucesso!",
"components.UserList.email": "Endereço de E-mail",
"components.UserList.deleteuser": "Apagar Utilizador",

View File

@@ -793,7 +793,8 @@
"components.UserList.usercreatedfailed": "Что-то пошло не так при создании пользователя.",
"components.UserList.passwordinfodescription": "Настройте URL-адрес приложения и включите уведомления по электронной почте, чтобы обеспечить возможность автоматической генерации пароля.",
"components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.",
"components.UserList.importfromplex": "Импортировать пользователей из {mediaServerName}",
"components.UserList.importfrommediaserver": "Импортировать пользователей из {mediaServerName}",
"components.UserList.importfromplex": "Импортировать пользователей из Plex",
"components.UserList.importedfromplex": "{userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!",
"components.UserList.edituser": "Изменить разрешения пользователя",
"components.UserList.displayName": "Отображаемое имя",

View File

@@ -1006,7 +1006,8 @@
"components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Diçka shkoi keq duke ruajtur cilësimet.",
"components.Settings.webAppUrlTip": "Në mënyrë opsionale drejto përdoruesit në aplikacionin web në serverin tënd në vend të atij web",
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minuta",
"components.UserList.importfromplex": "Importoni përdoruesit {mediaServerName}",
"components.UserList.importfrommediaserver": "Importoni përdoruesit {mediaServerName}",
"components.UserList.importfromplex": "Importoni përdoruesit Plex",
"components.UserList.importfromplexerror": "Diçka shkoi keq duke importuar përdoruesit Plex.",
"components.TvDetails.firstAirDate": "Data e parë e transmetimit",
"components.UserList.email": "Adresa email",

View File

@@ -222,7 +222,8 @@
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Versionsdata är för närvarande inte tillgänglig.",
"components.Settings.SettingsAbout.Releases.latestversion": "Senaste Versionen",
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
"components.UserList.importfromplex": "Importera {mediaServerName}användare",
"components.UserList.importfrommediaserver": "Importera {mediaServerName}användare",
"components.UserList.importfromplex": "Importera Plexanvändare",
"components.UserList.importfromplexerror": "Något gick fel när Plexanvändare importerades.",
"components.TvDetails.watchtrailer": "Kolla Trailer",
"components.Settings.Notifications.allowselfsigned": "Tillåt Självsignerade Certifikat",

View File

@@ -24,7 +24,8 @@
"components.UserList.localuser": "本地用户",
"components.UserList.localLoginDisabled": "<strong>允许本地登录</strong>的设置目前被禁用。",
"components.UserList.importfromplexerror": "导入 Plex 用户时出错。",
"components.UserList.importfromplex": "导入 {mediaServerName} 用户",
"components.UserList.importfrommediaserver": "导入 {mediaServerName} 用户",
"components.UserList.importfromplex": "导入 Plex 用户",
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} 成功导入!",
"components.UserList.email": "电子邮件地址",
"components.UserList.edituser": "编辑用户权限",

View File

@@ -52,7 +52,8 @@
"components.Settings.radarrsettings": "Radarr 設定",
"components.Settings.menuPlexSettings": "Plex",
"components.UserList.importfromplexerror": "匯入 Plex 使用者時出了點問題。",
"components.UserList.importfromplex": "匯入 {mediaServerName} 使用者",
"components.UserList.importfrommediaserver": "匯入 {mediaServerName} 使用者",
"components.UserList.importfromplex": "匯入 Plex 使用者",
"components.UserList.importedfromplex": "匯入 <strong>{userCount}</strong> 個 Plex 使用者成功!",
"components.UserList.localuser": "本地使用者",
"components.UserList.creating": "創建中…",

View File

@@ -2,6 +2,7 @@
const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
important: true,
mode: 'jit',
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
theme: {

View File

@@ -1622,6 +1622,13 @@
dependencies:
glob "7.1.7"
"@next/eslint-plugin-next@^12.1.6":
version "12.1.6"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
integrity sha512-yNUtJ90NEiYFT6TJnNyofKMPYqirKDwpahcbxBgSIuABwYOdkGwzos1ZkYD51Qf0diYwpQZBeVqElTk7Q2WNqw==
dependencies:
glob "7.1.7"
"@next/swc-android-arm64@12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
@@ -4780,6 +4787,11 @@ email-templates@^8.0.10:
nodemailer "^6.7.2"
preview-email "^3.0.5"
email-validator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
emoji-regex@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.1.tgz#77180edb279b99510a21b79b19e1dc283d8f3991"