Compare commits

..

69 Commits

Author SHA1 Message Date
semantic-release-bot
eceedbbaad chore(release): 1.1.0 2022-05-21 01:58:03 +00:00
Fallenbagel
29f06a965c Merge branch 'develop' 2022-05-21 06:43:52 +05:00
Fallenbagel
9ec05d3ba4 Fixed the link for the jellyseerr logo 2022-04-24 13:22:47 +05:00
semantic-release-bot
ee14ff5a51 chore(release): 1.0.2 2022-04-20 00:06:57 +00:00
Fallenbagel
6b62d4b862 Merge pull request #82 from Fallenbagel/workFlowfix
ci: adds GITHUB_TOKEN as an env
2022-04-20 05:02:17 +05:00
Fallenbagel
706fea0e97 ci: adds GITHUB_TOKEN as an env
adds GITHUB_TOKEN as an env to fix the github_token missing error during release workflow
2022-04-20 05:00:29 +05:00
Fallenbagel
80956d1a83 Merge pull request #81 from Fallenbagel/fixMediaServerType
fix: fix usertype from local user to mediaServerType
2022-04-20 04:58:20 +05:00
Fallenbagel
6d530d9028 fix: fix usertype from local user to mediaServerType
Fixes usertype from appearing as local user even if the mediaServerType is jellyfin
2022-04-20 04:52:39 +05:00
Fallenbagel
f12237565f Merge pull request #80 from Fallenbagel/packagejsonChanges
update tags and the branch to jellyseerr
2022-04-20 03:49:16 +05:00
Fallenbagel
11f5594ed4 update tags and the branch to jellyseerr 2022-04-20 03:47:46 +05:00
Fallenbagel
e4e58bee05 Merge pull request #79 from Fallenbagel/githubChanges
update workflows and discord locations for jellyseerr
2022-04-20 03:32:46 +05:00
Fallenbagel
13ee3a836c update workflows and discord locations for jellyseerr 2022-04-20 03:29:19 +05:00
Fallenbagel
3f16a353f5 Merge pull request #78 from Fallenbagel/urlValidationFix
fix: relax jellyfin url validation to allow local domains
2022-04-20 03:25:41 +05:00
Fallenbagel
9c43ba95e6 fix: relax jellyfin url validation to allow local domains
Relaxes jellyfin url validation so that http://localhost:8096 and http://jellyfin:8096 urls are
accepted in addition to full urls like https://example.com

fix #123
2022-04-20 03:12:01 +05:00
Fallenbagel
13fb6fd1a7 Updated the docker tags
Updated the docker tags to point to fallenbagel docker repo
2022-04-18 07:27:21 +05:00
Fallenbagel
16e8e3a38e update workflow to test for jellyseerr
update workflow and discord locations to test the docker pipeline
2022-04-18 07:17:16 +05:00
Fallenbagel
6fecdf094d Merge pull request #76 from Fallenbagel/updatePackagejson
Update package.json to reflect the jellyseerr version. This helps fix the version issue.
2022-04-15 14:57:27 +05:00
Fallenbagel
69b271b018 Chore(release):v1.0.1 2022-04-15 14:55:53 +05:00
Fallenbagel
d6ebd9a9b9 Chore(release):v1.0.1 2022-04-15 14:54:30 +05:00
Fallenbagel
70dad332fc Merge pull request #74 from Fallenbagel/versionStatusFix
fix: fix for the jellyseerr out of date even though it is up-to-date
2022-04-15 14:30:41 +05:00
Fallenbagel
a65e430c60 fix: fix for the jellyseerr out of date even though it is up-to-date
Reverting back the changes for the quick jellyseerr version fix for a better implementation
2022-04-15 14:03:03 +05:00
Fallenbagel
18f4b67b72 Merge pull request #73 from Fallenbagel/avatarfix
fix: fix default avatar missing
2022-04-15 12:12:13 +05:00
Fallenbagel
506c31562a fix: fix default avatar missing
Fix the default avatar missing because one of the os_logo_square.png file was missing
2022-04-15 12:07:12 +05:00
Fallenbagel
7a9d7a4834 Merge pull request #71 from jsl9208/feat-emby-mediaurl
feat: add emby detail url support
2022-04-15 11:21:49 +05:00
Fallenbagel
902a033b8a Merge pull request #72 from Fallenbagel/unknownjobfix
fix: replaced Unkown job with jellyfin in jobsandcache
2022-04-15 11:18:46 +05:00
Fallenbagel
00eb20aa5e fix: replaced Unkown job with jellyfin in jobsandcache
Replaced unknown job with jellyfin in jobsandcache and fixed the translations to reflect it as well
2022-04-15 10:46:09 +05:00
Shilong Jiang
a2c27cfa95 feat: add emby detail url support 2022-04-14 20:10:57 +08:00
Fallenbagel
7122b4d08b Replaced arm tags with latest
Replaced `:arm` and `:armv7` tags with `latest` as they are now deprecated.
2022-04-14 00:03:57 +05:00
Fallenbagel
b03b9b1dbb fix: fixed request card not displaying the requested season and episodes
When requested, the request card shows as {seasonCount, plural, one {Season}} and does not display
which season or episode was requested because it was still using the alpha request cards. This fixed
that issue
2022-04-13 17:24:47 +05:00
Fallenbagel
73672e29f8 fix: fixed jellyseerr out of date on stable version
When jellyseerr latest version or the stable version was deployed, the version was shown as out of
date with a message to up date to the latest version even though it was the latest version. This
fixed that issue
2022-04-13 17:21:03 +05:00
Fallenbagel
cc5192209f fixed logo_full.svg render 2022-04-13 13:17:54 +05:00
Fallenbagel
278dcf4b44 Update .all-contributorsrc 2022-04-13 13:17:54 +05:00
Fallenbagel
36e092f225 Update .all-contributorsrc 2022-04-13 13:17:54 +05:00
Fallenbagel
46d5c737a2 chore: github update 2022-04-13 13:17:54 +05:00
Fallenbagel
cba4878db3 feat: update zh_Hans.json
Update zh_Hans.json
2022-04-13 13:17:53 +05:00
Fallenbagel
57cc48a699 style: replaced Overseerr with jellyseerr 2022-04-13 13:17:53 +05:00
Fallenbagel
84f488be06 fix: database migration fix
Fixed the database migration issue fixing the error "SQLITE+ERROR: no such column:
User.jellyfinUsername
2022-04-13 13:17:53 +05:00
Fallenbagel
f885f2a0f3 ci: remove DEPENDABOT 2022-04-13 13:17:53 +05:00
Fallenbagel
eef3e5ea4c docs: added preview 2022-04-13 13:17:53 +05:00
Fallenbagel
8db821c1c1 docs: added new logo
Added new jellyseerr logo
2022-04-13 13:17:53 +05:00
Fallenbagel
a39b882f09 docs: added new logo
Added new jellyseerr logo
2022-04-13 13:17:53 +05:00
Fallenbagel
754dccc4bf first commit 2022-04-13 13:17:53 +05:00
Juan D. Jara
f97ee11430 fix(jellyfin): get jellyfin integration working with the last develop version
re #288
2021-09-27 02:56:02 +02:00
Juan D. Jara
54868fd486 style: fix linter and add types 2021-09-27 02:35:10 +02:00
Juan D. Jara
eea389879f Merge branch 'develop' of https://github.com/sct/overseerr into jellyfin-support 2021-09-27 02:24:30 +02:00
Aiden Vigue
5c917f95b4 fix(backend): use different device ids for jellyfin users 2021-06-17 13:42:08 -04:00
Aiden Vigue
dd4d42fd31 fix(backend): force same device id 2021-06-14 16:33:17 -04:00
Aiden Vigue
e5c6b9cd74 fix(backend): update jellyfin.ts for 10.8.0 2021-06-14 12:27:07 -04:00
Aiden Vigue
508fccae4e fix(build): fix build errors 2021-02-27 22:41:35 +00:00
Aiden Vigue
f77573c838 fix(frontend): revert mpaa change 2021-02-27 22:17:51 +00:00
Aiden Vigue
7dfe38001e fix(backend): fix Jellyfin scan for recently added items 2021-02-27 22:15:32 +00:00
Aiden Vigue
48f55da43e style(frontend): fix padding on MPAA rating 2021-02-27 22:15:32 +00:00
Aiden Vigue
1e97503802 fix(db): add migration 2021-02-27 22:13:53 +00:00
Aiden Vigue
42ff34bb3d fix(backend): remove console statement 2021-02-27 22:13:53 +00:00
Aiden Vigue
107b766c44 fix(frontend): add Jellyfin logo to ExternalLinkBlock 2021-02-27 22:13:23 +00:00
Aiden Vigue
fb51ce5570 feat(rebase): rebase 2021-02-27 22:12:55 +00:00
Aiden Vigue
3357343d98 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
9d61092f37 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
29274614c3 feat(rebase): rebase 2021-02-27 22:12:54 +00:00
Aiden Vigue
19b51592ea feat(rebase): rebase 2021-02-27 22:11:47 +00:00
Aiden Vigue
757c0fc29e feat(rebase): rebase 2021-02-27 22:11:27 +00:00
Aiden Vigue
3eb48abc14 feat(rebase): rebase 2021-02-27 22:11:27 +00:00
Aiden Vigue
01cd9d3872 feat(rebase): rebasse 2021-02-27 22:10:25 +00:00
Aiden Vigue
9582196e1f feat: rebase 2021-02-27 22:09:43 +00:00
Aiden Vigue
3743edab8d feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
d81e7cdbab feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
6e1d7f7075 feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
91cf2de33a feat(rebase): rebase 2021-02-27 22:09:02 +00:00
Aiden Vigue
a6ec2d5220 feat(all): add initial Jellyfin/Emby support 2021-02-27 22:09:02 +00:00
77 changed files with 1365 additions and 1874 deletions

7
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,7 @@
# Global code ownership
- @Fallenbagel
# i18n locale files
src/i18n/locale/ @Fallenbagel

View File

@@ -1,39 +0,0 @@
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'

3
.gitignore vendored
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,10 @@
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
## Current Features
- Jellyfin Support
- Emby Support
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
Along with all the existing Overseerr features:
@@ -35,18 +32,6 @@ 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 = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
failregex = .*\[info\]\[Auth\]\: 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,10 +2,6 @@ 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,36 +5815,6 @@ 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": "0.1.0",
"version": "1.1.0",
"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,7 +37,6 @@
"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",
@@ -85,7 +84,6 @@
"@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

@@ -0,0 +1,3 @@
[ZoneTransfer]
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
ZoneId=3

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import {
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import * as EmailValidator from 'email-validator';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
@@ -216,23 +215,14 @@ class EmailAgent
this.getSettings(),
payload.notifyUser.settings?.pgpKey
);
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,
});
}
await email.send(
this.buildMessage(
type,
payload,
payload.notifyUser.email,
payload.notifyUser.displayName
)
);
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',
@@ -278,18 +268,9 @@ class EmailAgent
this.getSettings(),
user.settings?.pgpKey
);
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,
});
}
await email.send(
this.buildMessage(type, payload, user.email, user.displayName)
);
} catch (e) {
logger.error('Error sending email notification', {
label: 'Notifications',

View File

@@ -134,7 +134,6 @@ interface FullPublicSettings extends PublicSettings {
enablePushRegistration: boolean;
locale: string;
emailEnabled: boolean;
userEmailRequired: boolean;
newPlexLogin: boolean;
}
@@ -160,7 +159,6 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
export interface NotificationAgentEmail extends NotificationAgentConfig {
options: {
userEmailRequired: boolean;
emailFrom: string;
smtpHost: string;
smtpPort: number;
@@ -337,7 +335,6 @@ class Settings {
email: {
enabled: false,
options: {
userEmailRequired: false,
emailFrom: '',
smtpHost: '',
smtpPort: 587,
@@ -532,8 +529,6 @@ 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,7 +9,6 @@ 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();
@@ -25,16 +24,6 @@ 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);
});
@@ -81,9 +70,6 @@ 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({
@@ -210,8 +196,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
settings.jellyfin.hostname !== ''
) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username) {
return res.status(500).json({ error: 'You must provide an username' });
} else if (!body.username || !body.password) {
return res
.status(500)
.json({ error: 'You must provide an username and a password' });
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
return res
.status(500)
@@ -225,7 +213,6 @@ 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({
@@ -242,10 +229,6 @@ 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
@@ -261,7 +244,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 = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
user.avatar = '/os_logo_square.png';
}
@@ -307,7 +290,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
@@ -336,7 +319,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
@@ -344,7 +327,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, IssueType } from '../constants/issue';
import { IssueStatus } from '../constants/issue';
import Issue from '../entity/Issue';
import IssueComment from '../entity/IssueComment';
import Media from '../entity/Media';
@@ -146,68 +146,6 @@ 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,11 +303,6 @@ 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({
@@ -326,7 +321,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
username: user.Name,
id: user.Id,
thumb: user.PrimaryImageTag
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
? `${settings.jellyfin.hostname}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
email: user.Name,
}));

View File

@@ -492,46 +492,56 @@ router.post(
);
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
const jellyfinUsersResponse = await jellyfinClient.getUsers();
const createdUsers: User[] = [];
const { hostname, externalHostname } = getSettings().jellyfin;
const jellyfinHost =
externalHostname && externalHostname.length > 0
? externalHostname
: hostname;
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();
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const jellyfinUsers = await jellyfinClient.getUsers();
const avatar = account.PrimaryImageTag
? `${settings.jellyfin.hostname}/Users/${account.Id}/Images/Primary/?tag=${account.PrimaryImageTag}&quality=90`
: '/os_logo_square.png';
for (const jellyfinUserId of body.jellyfinUserIds) {
const jellyfinUser = jellyfinUsers.users.find(
(user) => user.Id === jellyfinUserId
);
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;
const user = await userRepository.findOne({
select: ['id', 'jellyfinUserId'],
where: { jellyfinUserId: 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',
// });
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);
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);
}
}
}
return res.status(201).json(User.filterMany(createdUsers));
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -51,7 +51,6 @@ 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,
@@ -121,7 +120,6 @@ 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);
@@ -132,7 +130,6 @@ 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,12 +110,6 @@ 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 object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-10 w-10 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -35,7 +35,6 @@ 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}',
@@ -100,7 +99,6 @@ const IssueDetails: React.FC = () => {
(opt) => opt.issueType === issueData?.issueType
);
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -269,7 +267,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 object-cover 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 transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
src={issueData.createdBy.avatar}
alt=""
/>
@@ -368,18 +366,13 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
{intl.formatMessage(messages.playonplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.playonplex, {
mediaServerName: 'Jellyfin',
})}
? 'Plex'
: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -414,18 +407,13 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
{intl.formatMessage(messages.play4konplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Jellyfin',
})}
? 'Plex'
: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -630,18 +618,13 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
{intl.formatMessage(messages.playonplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? intl.formatMessage(messages.playonplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.playonplex, {
mediaServerName: 'Jellyfin',
})}
? 'Plex'
: 'Jellyfin',
})}
</span>
</Button>
)}
@@ -676,18 +659,13 @@ const IssueDetails: React.FC = () => {
>
<PlayIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
{intl.formatMessage(messages.play4konplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.play4konplex, {
mediaServerName: 'Jellyfin',
})}
? 'Plex'
: '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 object-cover"
className="avatar-sm ml-1.5"
/>
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{issue.createdBy.displayName}

View File

@@ -14,7 +14,6 @@ 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',
@@ -178,10 +177,6 @@ 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()} />
@@ -241,9 +236,6 @@ 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 object-cover sm:h-10 sm:w-10"
className="h-8 w-8 rounded-full sm:h-10 sm:w-10"
src={user?.avatar}
alt=""
/>

View File

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

View File

@@ -15,13 +15,12 @@ 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 {mediaServerName} account',
signinwithjellyfin: 'Use your Jellyfin account',
signinwithoverseerr: 'Use your {applicationTitle} account',
});
@@ -33,7 +32,6 @@ 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
@@ -135,12 +133,7 @@ const Login: React.FC = () => {
{settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: 'Jellyfin',
})}
: intl.formatMessage(messages.signinwithjellyfin)}
</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 object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full 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 object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
/>
</a>
</Link>

View File

@@ -48,7 +48,6 @@ 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',
@@ -96,7 +95,6 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
const minStudios = 3;
const [showMoreStudios, setShowMoreStudios] = useState(false);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -132,7 +130,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
if (data.mediaInfo?.mediaUrl) {
mediaLinks.push({
text: getAvalaibleMediaServerName(),
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
url: data.mediaInfo?.mediaUrl,
svg: <PlayIcon />,
});
@@ -145,7 +146,10 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
})
) {
mediaLinks.push({
text: getAvalaible4kMediaServerName(),
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
url: data.mediaInfo?.mediaUrl4k,
svg: <PlayIcon />,
});
@@ -224,30 +228,6 @@ 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,7 +26,6 @@ const messages = defineMessages({
server: 'Destination Server',
profilechanged: 'Quality Profile',
rootfolder: 'Root Folder',
languageprofile: 'Language Profile',
});
interface RequestBlockProps {
@@ -39,8 +38,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const { profile, rootFolder, server, languageProfile } =
useRequestOverride(request);
const { profile, rootFolder, server } = useRequestOverride(request);
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
setIsUpdating(true);
@@ -211,7 +209,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div>
</div>
)}
{(server || profile || rootFolder || languageProfile) && (
{(server || profile !== null || rootFolder) && (
<>
<div className="mt-4 mb-1 text-sm">
{intl.formatMessage(messages.requestoverrides)}
@@ -225,12 +223,12 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
<span>{server}</span>
</li>
)}
{profile && (
{profile !== null && (
<li className="flex justify-between px-1 py-2">
<span className="font-bold">
{intl.formatMessage(messages.profilechanged)}
</span>
<span>{profile}</span>
<span>ID {profile}</span>
</li>
)}
{rootFolder && (
@@ -241,14 +239,6 @@ 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 object-cover"
className="avatar-sm"
/>
<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 object-cover"
className="avatar-sm ml-1.5"
/>
<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 object-cover"
className="avatar-sm ml-1.5"
/>
<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 object-cover"
className="h-6 w-6 flex-shrink-0 rounded-full"
/>
<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 object-cover"
className="h-6 w-6 flex-shrink-0 rounded-full"
/>
<span className="ml-3 block flex-shrink-0">
{user.displayName}

View File

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

View File

@@ -10,11 +10,9 @@ 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';
@@ -104,7 +102,6 @@ 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 />;
@@ -372,33 +369,22 @@ const SettingsJobs: React.FC = () => {
</tr>
</thead>
<Table.TBody>
{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>
))}
{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>
))}
</Table.TBody>
</Table>
</div>

View File

@@ -1,8 +1,5 @@
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';
@@ -11,7 +8,7 @@ const messages = defineMessages({
menuGeneralSettings: 'General',
menuUsers: 'Users',
menuPlexSettings: 'Plex',
menuJellyfinSettings: '{mediaServerName}',
menuJellyfinSettings: 'Jellyfin',
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
@@ -21,8 +18,7 @@ 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),
@@ -34,17 +30,16 @@ const SettingsLayout: React.FC = ({ children }) => {
route: '/settings/users',
regex: /^\/settings\/users/,
},
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.menuPlexSettings),
route: '/settings/plex',
regex: /^\/settings\/plex/,
},
{
text: intl.formatMessage(messages.menuJellyfinSettings),
route: '/settings/jellyfin',
regex: /^\/settings\/jellyfin/,
},
{
text: intl.formatMessage(messages.menuServices),
route: '/settings/services',
@@ -81,12 +76,6 @@ 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,7 +14,6 @@ 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',
@@ -43,7 +42,6 @@ const SettingsUsers: React.FC = () => {
mutate: revalidate,
} = useSWR<MainSettings>('/api/v1/settings/main');
const settings = useSettings();
const { publicRuntimeConfig } = getConfig();
if (!data && !error) {
return <LoadingSpinner />;
@@ -133,20 +131,16 @@ const SettingsUsers: React.FC = () => {
<label htmlFor="newPlexLogin" className="checkbox-label">
{intl.formatMessage(messages.newPlexLogin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
<span className="label-tip">
{intl.formatMessage(messages.newPlexLoginTip, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? 'Emby'
: settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
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 Jellyseerr',
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by signing in with your Plex account',
});

View File

@@ -3,15 +3,14 @@ import { useUser } from '../../hooks/useUser';
import PlexLoginButton from '../PlexLoginButton';
import JellyfinLogin from '../Login/JellyfinLogin';
import axios from 'axios';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, FormattedMessage } 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 Jellyseerr',
welcome: 'Welcome to Overseerr',
signinMessage: 'Get started by signing in',
signinWithJellyfin: 'Use your {mediaServerName} account',
signinWithJellyfin: 'Use your Jellyfin account',
signinWithPlex: 'Use your Plex account',
});
@@ -25,8 +24,7 @@ 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.
@@ -93,13 +91,7 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
}`}
onClick={() => handleClick(1)}
>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Emby',
})
: intl.formatMessage(messages.signinWithJellyfin, {
mediaServerName: 'Jellyfin',
})}
<FormattedMessage {...messages.signinWithJellyfin} />
</button>
<AccordionContent isOpen={openIndexes.includes(1)}>
<div

View File

@@ -44,7 +44,6 @@ 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',
@@ -86,7 +85,6 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
router.query.manage == '1' ? true : false
);
const [showIssueModal, setShowIssueModal] = useState(false);
const { publicRuntimeConfig } = getConfig();
const {
data,
@@ -126,7 +124,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
mediaLinks.push({
text: getAvalaibleMediaServerName(),
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
url: data.mediaInfo?.mediaUrl,
svg: <PlayIcon />,
});
@@ -140,7 +141,10 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
})
) {
mediaLinks.push({
text: getAvalaible4kMediaServerName(),
text:
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
url: data.mediaInfo?.mediaUrl4k,
svg: <PlayIcon />,
});
@@ -224,30 +228,6 @@ 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,8 +8,6 @@ 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;
@@ -17,25 +15,23 @@ interface JellyfinImportProps {
}
const messages = defineMessages({
importfromJellyfin: 'Import {mediaServerName} Users',
importfromJellyfin: 'Import Jellyfin Users',
importfromJellyfinerror:
'Something went wrong while importing {mediaServerName} users.',
'Something went wrong while importing Jellyfin users.',
importedfromJellyfin:
'<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!',
'<strong>{userCount}</strong> Jellyfin {userCount, plural, one {user} other {users}} imported successfully!',
user: 'User',
noJellyfinuserstoimport: 'There are no {mediaServerName} users to import.',
noJellyfinuserstoimport: 'There are no Jellyfin users to import.',
newJellyfinsigninenabled:
'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.',
'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.',
});
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[]>([]);
@@ -51,18 +47,6 @@ 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);
@@ -82,8 +66,6 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
strong: function strong(msg) {
return <strong>{msg}</strong>;
},
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
@@ -95,16 +77,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
onComplete();
}
} catch (e) {
addToast(
intl.formatMessage(messages.importfromJellyfinerror, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
}),
{
autoDismiss: true,
appearance: 'error',
}
);
addToast(intl.formatMessage(messages.importfromJellyfinerror), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setImporting(false);
}
@@ -134,10 +110,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
return (
<Modal
loading={!data && !error}
title={intl.formatMessage(messages.importfromJellyfin, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
title={intl.formatMessage(messages.importfromJellyfin)}
iconSvg={<InboxInIcon />}
onOk={() => {
importUsers();
@@ -153,10 +126,6 @@ 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>
@@ -271,10 +240,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
</>
) : (
<Alert
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
mediaServerName:
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
})}
title={intl.formatMessage(messages.noJellyfinuserstoimport)}
type="info"
/>
)}

View File

@@ -9,7 +9,6 @@ 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';
@@ -41,7 +40,7 @@ import PlexImportModal from './PlexImportModal';
const messages = defineMessages({
users: 'Users',
userlist: 'User List',
importfrommediaserver: 'Import {mediaServerName} Users',
importfromplex: 'Import {mediaServerName} Users',
user: 'User',
totalrequests: 'Requests',
accounttype: 'Type',
@@ -88,7 +87,6 @@ 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');
@@ -482,9 +480,7 @@ const UserList: React.FC = () => {
setShowImportModal(false);
revalidate();
}}
>
{data.pageInfo.results}
</JellyfinImportModal>
/>
)}
</Transition>
@@ -507,18 +503,13 @@ const UserList: React.FC = () => {
>
<InboxInIcon />
<span>
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Emby',
})
: settings.currentSettings.mediaServerType ===
{intl.formatMessage(messages.importfromplex, {
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Plex',
})
: intl.formatMessage(messages.importfrommediaserver, {
mediaServerName: 'Jellyfin',
})}
? 'Plex'
: 'Jellyfin',
})}
</span>
</Button>
</div>
@@ -605,7 +596,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 object-cover"
className="h-10 w-10 rounded-full"
src={user.avatar}
alt=""
/>
@@ -645,23 +636,17 @@ 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: 'Emby',
})}
</Badge>
) : user.userType === UserType.JELLYFIN ? (
) : (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: '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 object-cover ring-1 ring-gray-700"
className="h-24 w-24 rounded-full bg-gray-600 ring-1 ring-gray-700"
src={user.avatar}
alt=""
/>

View File

@@ -7,6 +7,7 @@ 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,
@@ -24,13 +25,11 @@ 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',
@@ -60,7 +59,7 @@ const messages = defineMessages({
const UserGeneralSettings: React.FC = () => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings();
const { addToast } = useToasts();
const { locale, setLocale } = useLocale();
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
@@ -121,9 +120,8 @@ const UserGeneralSettings: React.FC = () => {
</div>
<Formik
initialValues={{
displayName: data?.username ?? '',
email: data?.email ?? '',
discordId: data?.discordId ?? '',
displayName: data?.username,
discordId: data?.discordId,
locale: data?.locale,
region: data?.region,
originalLanguage: data?.originalLanguage,
@@ -138,7 +136,6 @@ 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,
@@ -192,25 +189,19 @@ 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>
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
<Badge badgeType="success">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Emby',
})}
</Badge>
) : user?.userType === UserType.JELLYFIN ? (
) : (
<Badge badgeType="default">
{intl.formatMessage(messages.mediaServerUser, {
mediaServerName: 'Jellyfin',
mediaServerName:
settings.currentSettings.mediaServerType ===
MediaServerType.PLEX
? 'Plex'
: 'Jellyfin',
})}
</Badge>
) : null}
)}
</div>
</div>
</div>
@@ -248,32 +239,6 @@ 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,9 +82,7 @@ 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) ||
(!!data && (data[data?.length - 1]?.totalResults ?? 0) < 41);
isEmpty || (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20);
return {
isLoadingInitialData,

View File

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

View File

@@ -1,9 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import type { UrlObject } from 'url';
import type { Nullable } from '../utils/typeHelpers';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import useDebouncedState from './useDebouncedState';
import { useRouter } from 'next/router';
import type { Nullable } from '../utils/typeHelpers';
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 !== '' && searchOpen) {
if (debouncedValue !== '') {
if (router.pathname.startsWith('/search')) {
router.replace({
pathname: router.pathname,

View File

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

View File

@@ -686,8 +686,7 @@
"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.importfrommediaserver": "Importeu usuaris de {mediaServerName}",
"components.UserList.importfromplex": "Importeu usuaris de Plex",
"components.UserList.importfromplex": "Importeu usuaris de {mediaServerName}",
"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

@@ -540,4 +540,4 @@
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "ID chatu",
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Potvrďte heslo",
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Aktuální heslo"
}
}

View File

@@ -850,8 +850,7 @@
"components.UserList.nouserstoimport": "Ingen nye brugere som kan importeres fra Plex.",
"components.UserList.edituser": "Redigér Brugertilladelser",
"components.UserList.email": "Email Adresse",
"components.UserList.importfrommediaserver": "Importér Brugere fra {mediaServerName}",
"components.UserList.importfromplex": "Importér Brugere fra Plex",
"components.UserList.importfromplex": "Importér Brugere fra {mediaServerName}",
"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,8 +223,7 @@
"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.importfrommediaserver": "{mediaServerName}-Benutzer importieren",
"components.UserList.importfromplex": "Plex-Benutzer importieren",
"components.UserList.importfromplex": "{mediaServerName}-Benutzer importieren",
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien-Crew",
"components.PersonDetails.crewmember": "Crew",

View File

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

View File

@@ -269,7 +269,6 @@
"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",
@@ -861,8 +860,7 @@
"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.importfrommediaserver": "Import {mediaServerName} Users",
"components.UserList.importfromplex": "Import Plex Users",
"components.UserList.importfromplex": "Import {mediaServerName} 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,8 +223,7 @@
"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.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
"components.UserList.importfromplex": "Importar Usuarios de Plex",
"components.UserList.importfromplex": "Importar Usuarios de {mediaServerName}",
"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,8 +223,7 @@
"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.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
"components.UserList.importfromplex": "Importer les utilisateurs de Plex",
"components.UserList.importfromplex": "Importer les utilisateurs de {mediaServerName}",
"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,8 +165,7 @@
"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.importfrommediaserver": "Felhasználók importálása {mediaServerName}-ről",
"components.UserList.importfromplex": "Felhasználók importálása Plex-ről",
"components.UserList.importfromplex": "Felhasználók importálása {mediaServerName}-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,8 +223,7 @@
"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.importfrommediaserver": "Importa utenti {mediaServerName}",
"components.UserList.importfromplex": "Importa utenti Plex",
"components.UserList.importfromplex": "Importa utenti {mediaServerName}",
"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,8 +231,7 @@
"components.TvDetails.watchtrailer": "予告編を見る",
"components.MovieDetails.watchtrailer": "予告編を見る",
"components.UserList.importfromplexerror": "Plexからユーザーをインポート中に問題が発生しました。",
"components.UserList.importfrommediaserver": "{mediaServerName}からユーザーをインポート",
"components.UserList.importfromplex": "Plexからユーザーをインポート",
"components.UserList.importfromplex": "{mediaServerName}からユーザーをインポート",
"components.UserList.importedfromplex": "Plex から新ユーザー {userCount} 名をインポートしました。",
"components.TvDetails.viewfullcrew": "フルクルーを表示",
"components.TvDetails.firstAirDate": "初放送日",

View File

@@ -194,8 +194,7 @@
"components.UserList.userssaved": "Brukertillatelsene ble lagret!",
"components.UserList.users": "Brukere",
"components.UserList.importfromplexerror": "Noe gikk galt ved importering av brukere fra Plex.",
"components.UserList.importfrommediaserver": "Importer brukere fra {mediaServerName}",
"components.UserList.importfromplex": "Importer brukere fra Plex",
"components.UserList.importfromplex": "Importer brukere fra {mediaServerName}",
"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,8 +214,7 @@
"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.importfrommediaserver": "{mediaServerName}-gebruikers importeren",
"components.UserList.importfromplex": "Plex-gebruikers importeren",
"components.UserList.importfromplex": "{mediaServerName}-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,8 +962,7 @@
"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.importfrommediaserver": "Importuj użytkowników {mediaServerName}",
"components.UserList.importfromplex": "Importuj użytkowników Plex",
"components.UserList.importfromplex": "Importuj użytkowników {mediaServerName}",
"i18n.available": "Dostępny",
"components.UserList.sortDisplayName": "Wyświetlana nazwa",
"components.UserList.totalrequests": "Prośby",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1622,13 +1622,6 @@
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"
@@ -4787,11 +4780,6 @@ 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"