mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
Merge pull request #153 from Fallenbagel/develop
Merge branch 'develop'
This commit is contained in:
39
.github/workflows/private_registery_push.yml
vendored
Normal file
39
.github/workflows/private_registery_push.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: 'create docker image on pull request and push to private registery'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Login to private registery
|
||||||
|
uses: docker/login-action@v2.0.0
|
||||||
|
with:
|
||||||
|
registry: ${{ secrets.REGISTRY_URL }}
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./Dockerfile
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: true
|
||||||
|
tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}'
|
||||||
|
cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache'
|
||||||
|
cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max'
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
|||||||
jobs:
|
jobs:
|
||||||
semantic-release:
|
semantic-release:
|
||||||
name: Tag and release latest version
|
name: Tag and release latest version
|
||||||
runs-on: ubuntu-20.04
|
runs-on: self-hosted
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: semantic-release
|
needs: semantic-release
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-20.04
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Get Build Job Status
|
- name: Get Build Job Status
|
||||||
uses: technote-space/workflow-conclusion-action@v2
|
uses: technote-space/workflow-conclusion-action@v2
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,3 +53,6 @@ config/db/db.sqlite3-journal
|
|||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
|
||||||
|
# Webstorm
|
||||||
|
.idea
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -32,6 +32,18 @@ With more features on the way! Check out our [issue tracker](https://github.com/
|
|||||||
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
||||||
https://hub.docker.com/r/fallenbagel/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
|
## Preview
|
||||||
|
|
||||||
<img src="./public/preview.jpg">
|
<img src="./public/preview.jpg">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you
|
|||||||
|
|
||||||
```
|
```
|
||||||
[Definition]
|
[Definition]
|
||||||
failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
|
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.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
commitTag: process.env.COMMIT_TAG || 'local',
|
commitTag: process.env.COMMIT_TAG || 'local',
|
||||||
},
|
},
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
// Will be available on both server and client
|
||||||
|
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ['image.tmdb.org'],
|
domains: ['image.tmdb.org'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5815,6 +5815,36 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Issue'
|
$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}:
|
/issue/{issueId}:
|
||||||
get:
|
get:
|
||||||
summary: Get issue
|
summary: Get issue
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"country-flag-icons": "^1.4.21",
|
"country-flag-icons": "^1.4.21",
|
||||||
"csurf": "^1.11.0",
|
"csurf": "^1.11.0",
|
||||||
"email-templates": "^8.0.10",
|
"email-templates": "^8.0.10",
|
||||||
|
"email-validator": "^2.0.4",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"express-openapi-validator": "^4.13.6",
|
"express-openapi-validator": "^4.13.6",
|
||||||
"express-rate-limit": "^6.3.0",
|
"express-rate-limit": "^6.3.0",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"@babel/cli": "^7.17.6",
|
"@babel/cli": "^7.17.6",
|
||||||
"@commitlint/cli": "^16.2.1",
|
"@commitlint/cli": "^16.2.1",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
|
"@next/eslint-plugin-next": "^12.1.6",
|
||||||
"@semantic-release/changelog": "^6.0.1",
|
"@semantic-release/changelog": "^6.0.1",
|
||||||
"@semantic-release/commit-analyzer": "^9.0.2",
|
"@semantic-release/commit-analyzer": "^9.0.2",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface JellyfinLibraryItem {
|
|||||||
Id: string;
|
Id: string;
|
||||||
HasSubtitles: boolean;
|
HasSubtitles: boolean;
|
||||||
Type: 'Movie' | 'Episode' | 'Season' | 'Series';
|
Type: 'Movie' | 'Episode' | 'Season' | 'Series';
|
||||||
|
LocationType: 'FileSystem' | 'Offline' | 'Remote' | 'Virtual';
|
||||||
SeriesName?: string;
|
SeriesName?: string;
|
||||||
SeriesId?: string;
|
SeriesId?: string;
|
||||||
SeasonId?: string;
|
SeasonId?: string;
|
||||||
@@ -205,7 +206,9 @@ class JellyfinAPI {
|
|||||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
|
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return contents.data.Items;
|
return contents.data.Items.filter(
|
||||||
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||||
@@ -251,7 +254,9 @@ class JellyfinAPI {
|
|||||||
try {
|
try {
|
||||||
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||||
|
|
||||||
return contents.data.Items;
|
return contents.data.Items.filter(
|
||||||
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
||||||
@@ -270,7 +275,9 @@ class JellyfinAPI {
|
|||||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return contents.data.Items;
|
return contents.data.Items.filter(
|
||||||
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
||||||
|
|||||||
@@ -129,7 +129,13 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||||
params: { query, page, include_adult: includeAdult, language, year },
|
params: {
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
include_adult: includeAdult,
|
||||||
|
language,
|
||||||
|
primary_release_year: year,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export class User {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
public updatedAt: Date;
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
public warnings: string[] = [];
|
||||||
|
|
||||||
constructor(init?: Partial<User>) {
|
constructor(init?: Partial<User>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NotificationAgentKey } from '../../lib/settings';
|
|||||||
|
|
||||||
export interface UserSettingsGeneralResponse {
|
export interface UserSettingsGeneralResponse {
|
||||||
username?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
NotificationAgentKey,
|
NotificationAgentKey,
|
||||||
} from '../../settings';
|
} from '../../settings';
|
||||||
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
|
|
||||||
class EmailAgent
|
class EmailAgent
|
||||||
extends BaseAgent<NotificationAgentEmail>
|
extends BaseAgent<NotificationAgentEmail>
|
||||||
@@ -215,6 +216,7 @@ class EmailAgent
|
|||||||
this.getSettings(),
|
this.getSettings(),
|
||||||
payload.notifyUser.settings?.pgpKey
|
payload.notifyUser.settings?.pgpKey
|
||||||
);
|
);
|
||||||
|
if (EmailValidator.validate(payload.notifyUser.email)) {
|
||||||
await email.send(
|
await email.send(
|
||||||
this.buildMessage(
|
this.buildMessage(
|
||||||
type,
|
type,
|
||||||
@@ -223,6 +225,14 @@ class EmailAgent
|
|||||||
payload.notifyUser.displayName
|
payload.notifyUser.displayName
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn('Invalid email address provided for user', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: payload.notifyUser.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending email notification', {
|
logger.error('Error sending email notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
@@ -268,9 +278,18 @@ class EmailAgent
|
|||||||
this.getSettings(),
|
this.getSettings(),
|
||||||
user.settings?.pgpKey
|
user.settings?.pgpKey
|
||||||
);
|
);
|
||||||
|
if (EmailValidator.validate(user.email)) {
|
||||||
await email.send(
|
await email.send(
|
||||||
this.buildMessage(type, payload, user.email, user.displayName)
|
this.buildMessage(type, payload, user.email, user.displayName)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn('Invalid email address provided for user', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error sending email notification', {
|
logger.error('Error sending email notification', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ interface FullPublicSettings extends PublicSettings {
|
|||||||
enablePushRegistration: boolean;
|
enablePushRegistration: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
userEmailRequired: boolean;
|
||||||
newPlexLogin: boolean;
|
newPlexLogin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +160,7 @@ export interface NotificationAgentSlack extends NotificationAgentConfig {
|
|||||||
|
|
||||||
export interface NotificationAgentEmail extends NotificationAgentConfig {
|
export interface NotificationAgentEmail extends NotificationAgentConfig {
|
||||||
options: {
|
options: {
|
||||||
|
userEmailRequired: boolean;
|
||||||
emailFrom: string;
|
emailFrom: string;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
smtpPort: number;
|
smtpPort: number;
|
||||||
@@ -335,6 +337,7 @@ class Settings {
|
|||||||
email: {
|
email: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
options: {
|
options: {
|
||||||
|
userEmailRequired: false,
|
||||||
emailFrom: '',
|
emailFrom: '',
|
||||||
smtpHost: '',
|
smtpHost: '',
|
||||||
smtpPort: 587,
|
smtpPort: 587,
|
||||||
@@ -342,7 +345,7 @@ class Settings {
|
|||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
requireTls: false,
|
requireTls: false,
|
||||||
allowSelfSigned: false,
|
allowSelfSigned: false,
|
||||||
senderName: 'Overseerr',
|
senderName: 'Jellyseerr',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
@@ -529,6 +532,8 @@ class Settings {
|
|||||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||||
locale: this.data.main.locale,
|
locale: this.data.main.locale,
|
||||||
emailEnabled: this.data.notifications.agents.email.enabled,
|
emailEnabled: this.data.notifications.agents.email.enabled,
|
||||||
|
userEmailRequired:
|
||||||
|
this.data.notifications.agents.email.options.userEmailRequired,
|
||||||
newPlexLogin: this.data.main.newPlexLogin,
|
newPlexLogin: this.data.main.newPlexLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Permission } from '../lib/permissions';
|
|||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { isAuthenticated } from '../middleware/auth';
|
import { isAuthenticated } from '../middleware/auth';
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
|
|
||||||
@@ -24,6 +25,16 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
|||||||
where: { id: req.user.id },
|
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);
|
return res.status(200).json(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,6 +81,9 @@ authRoutes.post('/plex', async (req, res, next) => {
|
|||||||
userType: UserType.PLEX,
|
userType: UserType.PLEX,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settings.main.mediaServerType = MediaServerType.PLEX;
|
||||||
|
settings.save();
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
} else {
|
} else {
|
||||||
const mainUser = await userRepository.findOneOrFail({
|
const mainUser = await userRepository.findOneOrFail({
|
||||||
@@ -196,10 +210,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.hostname !== ''
|
settings.jellyfin.hostname !== ''
|
||||||
) {
|
) {
|
||||||
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
||||||
} else if (!body.username || !body.password) {
|
} else if (!body.username) {
|
||||||
return res
|
return res.status(500).json({ error: 'You must provide an username' });
|
||||||
.status(500)
|
|
||||||
.json({ error: 'You must provide an username and a password' });
|
|
||||||
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
|
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
@@ -213,6 +225,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
settings.jellyfin.hostname !== ''
|
settings.jellyfin.hostname !== ''
|
||||||
? settings.jellyfin.hostname
|
? settings.jellyfin.hostname
|
||||||
: body.hostname;
|
: body.hostname;
|
||||||
|
const { externalHostname } = getSettings().jellyfin;
|
||||||
|
|
||||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||||
let user = await userRepository.findOne({
|
let user = await userRepository.findOne({
|
||||||
@@ -229,6 +242,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
// First we need to attempt to log the user in to jellyfin
|
// First we need to attempt to log the user in to jellyfin
|
||||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||||
|
const jellyfinHost =
|
||||||
|
externalHostname && externalHostname.length > 0
|
||||||
|
? externalHostname
|
||||||
|
: hostname;
|
||||||
|
|
||||||
const account = await jellyfinserver.login(body.username, body.password);
|
const account = await jellyfinserver.login(body.username, body.password);
|
||||||
// Next let's see if the user already exists
|
// Next let's see if the user already exists
|
||||||
@@ -244,7 +261,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
|
|
||||||
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
// Update the users avatar with their jellyfin profile pic (incase it changed)
|
||||||
if (account.User.PrimaryImageTag) {
|
if (account.User.PrimaryImageTag) {
|
||||||
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||||
} else {
|
} else {
|
||||||
user.avatar = '/os_logo_square.png';
|
user.avatar = '/os_logo_square.png';
|
||||||
}
|
}
|
||||||
@@ -290,7 +307,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: '/os_logo_square.png',
|
: '/os_logo_square.png',
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
@@ -319,7 +336,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: account.User.PrimaryImageTag
|
avatar: account.User.PrimaryImageTag
|
||||||
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||||
: '/os_logo_square.png',
|
: '/os_logo_square.png',
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
@@ -327,7 +344,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
const passedExplicitPassword =
|
const passedExplicitPassword =
|
||||||
body.password && body.password.length > 0;
|
body.password && body.password.length > 0;
|
||||||
if (passedExplicitPassword) {
|
if (passedExplicitPassword) {
|
||||||
await user.setPassword(body.password);
|
await user.setPassword(body.password ?? '');
|
||||||
}
|
}
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { IssueStatus } from '../constants/issue';
|
import { IssueStatus, IssueType } from '../constants/issue';
|
||||||
import Issue from '../entity/Issue';
|
import Issue from '../entity/Issue';
|
||||||
import IssueComment from '../entity/IssueComment';
|
import IssueComment from '../entity/IssueComment';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
@@ -146,6 +146,68 @@ issueRoutes.post<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
issueRoutes.get('/count', async (req, res, next) => {
|
||||||
|
const issueRepository = getRepository(Issue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query = issueRepository.createQueryBuilder('issue');
|
||||||
|
|
||||||
|
const totalCount = await query.getCount();
|
||||||
|
|
||||||
|
const videoCount = await query
|
||||||
|
.where('issue.issueType = :issueType', {
|
||||||
|
issueType: IssueType.VIDEO,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
const audioCount = await query
|
||||||
|
.where('issue.issueType = :issueType', {
|
||||||
|
issueType: IssueType.AUDIO,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
const subtitlesCount = await query
|
||||||
|
.where('issue.issueType = :issueType', {
|
||||||
|
issueType: IssueType.SUBTITLES,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
const othersCount = await query
|
||||||
|
.where('issue.issueType = :issueType', {
|
||||||
|
issueType: IssueType.OTHER,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
const openCount = await query
|
||||||
|
.where('issue.status = :issueStatus', {
|
||||||
|
issueStatus: IssueStatus.OPEN,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
const closedCount = await query
|
||||||
|
.where('issue.status = :issueStatus', {
|
||||||
|
issueStatus: IssueStatus.RESOLVED,
|
||||||
|
})
|
||||||
|
.getCount();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
total: totalCount,
|
||||||
|
video: videoCount,
|
||||||
|
audio: audioCount,
|
||||||
|
subtitles: subtitlesCount,
|
||||||
|
others: othersCount,
|
||||||
|
open: openCount,
|
||||||
|
closed: closedCount,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Something went wrong retrieving issue counts.', {
|
||||||
|
label: 'API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
next({ status: 500, message: 'Unable to retrieve issue counts.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
issueRoutes.get<{ issueId: string }>(
|
issueRoutes.get<{ issueId: string }>(
|
||||||
'/:issueId',
|
'/:issueId',
|
||||||
isAuthenticated(
|
isAuthenticated(
|
||||||
|
|||||||
@@ -303,6 +303,11 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
|||||||
|
|
||||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||||
|
const jellyfinHost =
|
||||||
|
externalHostname && externalHostname.length > 0
|
||||||
|
? externalHostname
|
||||||
|
: hostname;
|
||||||
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOneOrFail({
|
const admin = await userRepository.findOneOrFail({
|
||||||
@@ -321,7 +326,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
username: user.Name,
|
username: user.Name,
|
||||||
id: user.Id,
|
id: user.Id,
|
||||||
thumb: user.PrimaryImageTag
|
thumb: user.PrimaryImageTag
|
||||||
? `${settings.jellyfin.hostname}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
|
||||||
: '/os_logo_square.png',
|
: '/os_logo_square.png',
|
||||||
email: user.Name,
|
email: user.Name,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -492,56 +492,46 @@ router.post(
|
|||||||
);
|
);
|
||||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
|
|
||||||
const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||||
const createdUsers: User[] = [];
|
const createdUsers: User[] = [];
|
||||||
for (const account of jellyfinUsersResponse.users) {
|
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||||
if (account.Name) {
|
const jellyfinHost =
|
||||||
const user = await userRepository
|
externalHostname && externalHostname.length > 0
|
||||||
.createQueryBuilder('user')
|
? externalHostname
|
||||||
.where('user.jellyfinUserId = :id', { id: account.Id })
|
: hostname;
|
||||||
.orWhere('user.email = :email', {
|
|
||||||
email: account.Name,
|
|
||||||
})
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
const avatar = account.PrimaryImageTag
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
? `${settings.jellyfin.hostname}/Users/${account.Id}/Images/Primary/?tag=${account.PrimaryImageTag}&quality=90`
|
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||||
: '/os_logo_square.png';
|
|
||||||
|
|
||||||
if (user) {
|
for (const jellyfinUserId of body.jellyfinUserIds) {
|
||||||
// Update the user's avatar with their Jellyfin thumbnail, in case it changed
|
const jellyfinUser = jellyfinUsers.users.find(
|
||||||
user.avatar = avatar;
|
(user) => user.Id === jellyfinUserId
|
||||||
user.email = account.Name;
|
);
|
||||||
user.jellyfinUsername = account.Name;
|
|
||||||
|
|
||||||
// In case the user was previously a local account
|
const user = await userRepository.findOne({
|
||||||
if (user.userType === UserType.LOCAL) {
|
select: ['id', 'jellyfinUserId'],
|
||||||
user.userType = UserType.JELLYFIN;
|
where: { jellyfinUserId: jellyfinUserId },
|
||||||
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({
|
const newUser = new User({
|
||||||
jellyfinUsername: account.Name,
|
jellyfinUsername: jellyfinUser?.Name,
|
||||||
jellyfinUserId: account.Id,
|
jellyfinUserId: jellyfinUser?.Id,
|
||||||
jellyfinDeviceId: Buffer.from(
|
jellyfinDeviceId: Buffer.from(
|
||||||
`BOT_overseerr_${account.Name ?? ''}`
|
`BOT_jellyseerr_${jellyfinUser?.Name ?? ''}`
|
||||||
).toString('base64'),
|
).toString('base64'),
|
||||||
email: account.Name,
|
email: jellyfinUser?.Name,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar,
|
avatar: jellyfinUser?.PrimaryImageTag
|
||||||
|
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
|
||||||
|
: '/os_logo_square.png',
|
||||||
userType: UserType.JELLYFIN,
|
userType: UserType.JELLYFIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
await userRepository.save(newUser);
|
await userRepository.save(newUser);
|
||||||
createdUsers.push(newUser);
|
createdUsers.push(newUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(201).json(User.filterMany(createdUsers));
|
return res.status(201).json(User.filterMany(createdUsers));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
|||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
discordId: user.settings?.discordId,
|
discordId: user.settings?.discordId,
|
||||||
locale: user.settings?.locale,
|
locale: user.settings?.locale,
|
||||||
region: user.settings?.region,
|
region: user.settings?.region,
|
||||||
@@ -120,6 +121,7 @@ userSettingsRoutes.post<
|
|||||||
user.settings.locale = req.body.locale;
|
user.settings.locale = req.body.locale;
|
||||||
user.settings.region = req.body.region;
|
user.settings.region = req.body.region;
|
||||||
user.settings.originalLanguage = req.body.originalLanguage;
|
user.settings.originalLanguage = req.body.originalLanguage;
|
||||||
|
user.email = req.body.email ?? user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
@@ -130,6 +132,7 @@ userSettingsRoutes.post<
|
|||||||
locale: user.settings.locale,
|
locale: user.settings.locale,
|
||||||
region: user.settings.region,
|
region: user.settings.region,
|
||||||
originalLanguage: user.settings.originalLanguage,
|
originalLanguage: user.settings.originalLanguage,
|
||||||
|
email: user.email,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ const networks: Network[] = [
|
|||||||
'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png',
|
'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/nm8d7P7MJNiBLdgIzUK0gkuEA4r.png',
|
||||||
url: '/discover/tv/network/16',
|
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',
|
name: 'BBC One',
|
||||||
image:
|
image:
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const IssueComment: React.FC<IssueCommentProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={comment.user.avatar}
|
src={comment.user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-10 w-10 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import IssueComment from './IssueComment';
|
|||||||
import IssueDescription from './IssueDescription';
|
import IssueDescription from './IssueDescription';
|
||||||
import { MediaServerType } from '../../../server/constants/server';
|
import { MediaServerType } from '../../../server/constants/server';
|
||||||
import useSettings from '../../hooks/useSettings';
|
import useSettings from '../../hooks/useSettings';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
openedby: '#{issueId} opened {relativeTime} by {username}',
|
openedby: '#{issueId} opened {relativeTime} by {username}',
|
||||||
@@ -99,6 +100,7 @@ const IssueDetails: React.FC = () => {
|
|||||||
(opt) => opt.issueType === issueData?.issueType
|
(opt) => opt.issueType === issueData?.issueType
|
||||||
);
|
);
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -267,7 +269,7 @@ const IssueDetails: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5">
|
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5">
|
||||||
<img
|
<img
|
||||||
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
||||||
src={issueData.createdBy.avatar}
|
src={issueData.createdBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
@@ -366,12 +368,17 @@ const IssueDetails: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.playonplex, {
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
mediaServerName:
|
? intl.formatMessage(messages.playonplex, {
|
||||||
settings.currentSettings.mediaServerType ===
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? intl.formatMessage(messages.playonplex, {
|
||||||
: 'Jellyfin',
|
mediaServerName: 'Plex',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.playonplex, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -407,12 +414,17 @@ const IssueDetails: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.play4konplex, {
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
mediaServerName:
|
? intl.formatMessage(messages.play4konplex, {
|
||||||
settings.currentSettings.mediaServerType ===
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? intl.formatMessage(messages.play4konplex, {
|
||||||
: 'Jellyfin',
|
mediaServerName: 'Plex',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.play4konplex, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -618,12 +630,17 @@ const IssueDetails: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.playonplex, {
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
mediaServerName:
|
? intl.formatMessage(messages.playonplex, {
|
||||||
settings.currentSettings.mediaServerType ===
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? intl.formatMessage(messages.playonplex, {
|
||||||
: 'Jellyfin',
|
mediaServerName: 'Plex',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.playonplex, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -659,12 +676,17 @@ const IssueDetails: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.play4konplex, {
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
mediaServerName:
|
? intl.formatMessage(messages.play4konplex, {
|
||||||
settings.currentSettings.mediaServerType ===
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? intl.formatMessage(messages.play4konplex, {
|
||||||
: 'Jellyfin',
|
mediaServerName: 'Plex',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.play4konplex, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ const IssueItem: React.FC<IssueItemProps> = ({ issue }) => {
|
|||||||
<img
|
<img
|
||||||
src={issue.createdBy.avatar}
|
src={issue.createdBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm ml-1.5"
|
className="avatar-sm ml-1.5 object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||||
{issue.createdBy.displayName}
|
{issue.createdBy.displayName}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import useClickOutside from '../../../hooks/useClickOutside';
|
|||||||
import { Permission, useUser } from '../../../hooks/useUser';
|
import { Permission, useUser } from '../../../hooks/useUser';
|
||||||
import Transition from '../../Transition';
|
import Transition from '../../Transition';
|
||||||
import VersionStatus from '../VersionStatus';
|
import VersionStatus from '../VersionStatus';
|
||||||
|
import UserWarnings from '../UserWarnings';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
dashboard: 'Discover',
|
dashboard: 'Discover',
|
||||||
@@ -177,6 +178,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="px-2">
|
||||||
|
<UserWarnings onClick={() => setClosed()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasPermission(Permission.ADMIN) && (
|
{hasPermission(Permission.ADMIN) && (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<VersionStatus onClick={() => setClosed()} />
|
<VersionStatus onClick={() => setClosed()} />
|
||||||
@@ -236,6 +241,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="px-2">
|
||||||
|
<UserWarnings />
|
||||||
|
</div>
|
||||||
{hasPermission(Permission.ADMIN) && (
|
{hasPermission(Permission.ADMIN) && (
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<VersionStatus />
|
<VersionStatus />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const UserDropdown: React.FC = () => {
|
|||||||
onClick={() => setDropdownOpen(true)}
|
onClick={() => setDropdownOpen(true)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="h-8 w-8 rounded-full sm:h-10 sm:w-10"
|
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||||
src={user?.avatar}
|
src={user?.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
66
src/components/Layout/UserWarnings/index.tsx
Normal file
66
src/components/Layout/UserWarnings/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useUser } from '../../../hooks/useUser';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
emailRequired: 'An email address is required.',
|
||||||
|
emailInvalid: 'Email address is invalid.',
|
||||||
|
passwordRequired: 'A password is required.',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UserWarningsProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { user } = useUser();
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = null;
|
||||||
|
|
||||||
|
//check if a user has warnings
|
||||||
|
if (user.warnings.length > 0) {
|
||||||
|
user.warnings.forEach((warning) => {
|
||||||
|
let link = '';
|
||||||
|
let warningText = '';
|
||||||
|
let warningTitle = '';
|
||||||
|
switch (warning) {
|
||||||
|
case 'userEmailRequired':
|
||||||
|
link = '/profile/settings/';
|
||||||
|
warningTitle = 'Profile is incomplete';
|
||||||
|
warningText = intl.formatMessage(messages.emailRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
res = (
|
||||||
|
<Link href={link}>
|
||||||
|
<a
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
|
||||||
|
>
|
||||||
|
<ExclamationIcon className="h-6 w-6" />
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||||
|
<span className="font-bold">{warningTitle}</span>
|
||||||
|
<span className="truncate">{warningText}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserWarnings;
|
||||||
@@ -50,6 +50,7 @@ const Layout: React.FC = ({ children }) => {
|
|||||||
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
|
<div className="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 className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
|
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
|
||||||
|
|
||||||
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import Button from '../Common/Button';
|
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import * as Yup from 'yup';
|
||||||
import useSettings from '../../hooks/useSettings';
|
import useSettings from '../../hooks/useSettings';
|
||||||
|
import Button from '../Common/Button';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
host: 'Jellyfin URL',
|
host: '{mediaServerName} URL',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
validationhostrequired: 'Jellyfin URL required',
|
validationhostrequired: '{mediaServerName} URL required',
|
||||||
validationhostformat: 'Valid URL required',
|
validationhostformat: 'Valid URL required',
|
||||||
validationemailrequired: 'Email required',
|
validationemailrequired: 'Email required',
|
||||||
validationemailformat: 'Valid email required',
|
validationemailformat: 'Valid email required',
|
||||||
@@ -40,6 +40,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
const toasts = useToasts();
|
const toasts = useToasts();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
const LoginSchema = Yup.object().shape({
|
const LoginSchema = Yup.object().shape({
|
||||||
@@ -48,16 +49,19 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
|
/^(?:(?:(?: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)
|
intl.formatMessage(messages.validationhostformat)
|
||||||
)
|
)
|
||||||
.required(intl.formatMessage(messages.validationhostrequired)),
|
.required(
|
||||||
|
intl.formatMessage(messages.validationhostrequired, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
|
})
|
||||||
|
),
|
||||||
email: Yup.string()
|
email: Yup.string()
|
||||||
.email(intl.formatMessage(messages.validationemailformat))
|
.email(intl.formatMessage(messages.validationemailformat))
|
||||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
.required(intl.formatMessage(messages.validationemailrequired)),
|
||||||
username: Yup.string().required(
|
username: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationusernamerequired)
|
intl.formatMessage(messages.validationusernamerequired)
|
||||||
),
|
),
|
||||||
password: Yup.string().required(
|
password: Yup.string(),
|
||||||
intl.formatMessage(messages.validationpasswordrequired)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
@@ -97,7 +101,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
<Form>
|
<Form>
|
||||||
<div className="sm:border-t sm:border-gray-800">
|
<div className="sm:border-t sm:border-gray-800">
|
||||||
<label htmlFor="host" className="text-label">
|
<label htmlFor="host" className="text-label">
|
||||||
{intl.formatMessage(messages.host)}
|
{intl.formatMessage(messages.host, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
})}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||||
<div className="flex rounded-md shadow-sm">
|
<div className="flex rounded-md shadow-sm">
|
||||||
@@ -105,7 +114,12 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
id="host"
|
id="host"
|
||||||
name="host"
|
name="host"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={intl.formatMessage(messages.host)}
|
placeholder={intl.formatMessage(messages.host, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.host && touched.host && (
|
{errors.host && touched.host && (
|
||||||
@@ -185,9 +199,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
username: Yup.string().required(
|
username: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationusernamerequired)
|
intl.formatMessage(messages.validationusernamerequired)
|
||||||
),
|
),
|
||||||
password: Yup.string().required(
|
password: Yup.string(),
|
||||||
intl.formatMessage(messages.validationpasswordrequired)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -266,8 +278,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
as="a"
|
as="a"
|
||||||
buttonType="ghost"
|
buttonType="ghost"
|
||||||
href={
|
href={
|
||||||
settings.currentSettings.jellyfinHost +
|
process.env.JELLYFIN_TYPE == 'emby'
|
||||||
'/web/#!/forgotpassword.html'
|
? settings.currentSettings.jellyfinHost +
|
||||||
|
'/web/index.html#!/startup/forgotpassword.html'
|
||||||
|
: settings.currentSettings.jellyfinHost +
|
||||||
|
'/web/index.html#!/forgotpassword.html'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.forgotpassword)}
|
{intl.formatMessage(messages.forgotpassword)}
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import PlexLoginButton from '../PlexLoginButton';
|
|||||||
import Transition from '../Transition';
|
import Transition from '../Transition';
|
||||||
import JellyfinLogin from './JellyfinLogin';
|
import JellyfinLogin from './JellyfinLogin';
|
||||||
import LocalLogin from './LocalLogin';
|
import LocalLogin from './LocalLogin';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
signin: 'Sign In',
|
signin: 'Sign In',
|
||||||
signinheader: 'Sign in to continue',
|
signinheader: 'Sign in to continue',
|
||||||
signinwithplex: 'Use your Plex account',
|
signinwithplex: 'Use your Plex account',
|
||||||
signinwithjellyfin: 'Use your Jellyfin account',
|
signinwithjellyfin: 'Use your {mediaServerName} account',
|
||||||
signinwithoverseerr: 'Use your {applicationTitle} account',
|
signinwithoverseerr: 'Use your {applicationTitle} account',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ const Login: React.FC = () => {
|
|||||||
const { user, revalidate } = useUser();
|
const { user, revalidate } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
|
// 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
|
// We take the token and attempt to sign in. If we get a success message, we will
|
||||||
@@ -133,7 +135,12 @@ const Login: React.FC = () => {
|
|||||||
{settings.currentSettings.mediaServerType ==
|
{settings.currentSettings.mediaServerType ==
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? intl.formatMessage(messages.signinwithplex)
|
? intl.formatMessage(messages.signinwithplex)
|
||||||
: intl.formatMessage(messages.signinwithjellyfin)}
|
: intl.formatMessage(messages.signinwithjellyfin, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
})}
|
||||||
</button>
|
</button>
|
||||||
<AccordionContent isOpen={openIndexes.includes(0)}>
|
<AccordionContent isOpen={openIndexes.includes(0)}>
|
||||||
<div className="px-10 py-8">
|
<div className="px-10 py-8">
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ const ManageSlideOver: React.FC<
|
|||||||
{hasPermission(Permission.ADMIN) &&
|
{hasPermission(Permission.ADMIN) &&
|
||||||
(data.mediaInfo?.serviceUrl ||
|
(data.mediaInfo?.serviceUrl ||
|
||||||
data.mediaInfo?.tautulliUrl ||
|
data.mediaInfo?.tautulliUrl ||
|
||||||
watchData?.data?.playCount) && (
|
!!watchData?.data?.playCount) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 text-xl font-bold">
|
<h3 className="mb-2 text-xl font-bold">
|
||||||
{intl.formatMessage(messages.manageModalMedia)}
|
{intl.formatMessage(messages.manageModalMedia)}
|
||||||
@@ -272,7 +272,7 @@ const ManageSlideOver: React.FC<
|
|||||||
<img
|
<img
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -325,7 +325,7 @@ const ManageSlideOver: React.FC<
|
|||||||
{hasPermission(Permission.ADMIN) &&
|
{hasPermission(Permission.ADMIN) &&
|
||||||
(data.mediaInfo?.serviceUrl4k ||
|
(data.mediaInfo?.serviceUrl4k ||
|
||||||
data.mediaInfo?.tautulliUrl4k ||
|
data.mediaInfo?.tautulliUrl4k ||
|
||||||
watchData?.data4k?.playCount) && (
|
!!watchData?.data4k?.playCount) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 text-xl font-bold">
|
<h3 className="mb-2 text-xl font-bold">
|
||||||
{intl.formatMessage(messages.manageModalMedia4k)}
|
{intl.formatMessage(messages.manageModalMedia4k)}
|
||||||
@@ -387,7 +387,7 @@ const ManageSlideOver: React.FC<
|
|||||||
<img
|
<img
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
className="h-8 w-8 scale-100 transform-gpu rounded-full ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import PersonCard from '../PersonCard';
|
|||||||
import RequestButton from '../RequestButton';
|
import RequestButton from '../RequestButton';
|
||||||
import Slider from '../Slider';
|
import Slider from '../Slider';
|
||||||
import StatusBadge from '../StatusBadge';
|
import StatusBadge from '../StatusBadge';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
originaltitle: 'Original Title',
|
originaltitle: 'Original Title',
|
||||||
@@ -95,6 +96,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
const minStudios = 3;
|
const minStudios = 3;
|
||||||
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
const [showMoreStudios, setShowMoreStudios] = useState(false);
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -130,10 +132,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
|
|
||||||
if (data.mediaInfo?.mediaUrl) {
|
if (data.mediaInfo?.mediaUrl) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text:
|
text: getAvalaibleMediaServerName(),
|
||||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
|
|
||||||
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
|
|
||||||
url: data.mediaInfo?.mediaUrl,
|
url: data.mediaInfo?.mediaUrl,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -146,10 +145,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text:
|
text: getAvalaible4kMediaServerName(),
|
||||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
|
|
||||||
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
|
|
||||||
url: data.mediaInfo?.mediaUrl4k,
|
url: data.mediaInfo?.mediaUrl4k,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -228,6 +224,30 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
|
|||||||
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
||||||
?.flatrate ?? [];
|
?.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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="media-page"
|
className="media-page"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const messages = defineMessages({
|
|||||||
server: 'Destination Server',
|
server: 'Destination Server',
|
||||||
profilechanged: 'Quality Profile',
|
profilechanged: 'Quality Profile',
|
||||||
rootfolder: 'Root Folder',
|
rootfolder: 'Root Folder',
|
||||||
|
languageprofile: 'Language Profile',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface RequestBlockProps {
|
interface RequestBlockProps {
|
||||||
@@ -38,7 +39,8 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const { profile, rootFolder, server } = useRequestOverride(request);
|
const { profile, rootFolder, server, languageProfile } =
|
||||||
|
useRequestOverride(request);
|
||||||
|
|
||||||
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
|
const updateRequest = async (type: 'approve' | 'decline'): Promise<void> => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
@@ -209,7 +211,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(server || profile !== null || rootFolder) && (
|
{(server || profile || rootFolder || languageProfile) && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 mb-1 text-sm">
|
<div className="mt-4 mb-1 text-sm">
|
||||||
{intl.formatMessage(messages.requestoverrides)}
|
{intl.formatMessage(messages.requestoverrides)}
|
||||||
@@ -223,12 +225,12 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
|||||||
<span>{server}</span>
|
<span>{server}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{profile !== null && (
|
{profile && (
|
||||||
<li className="flex justify-between px-1 py-2">
|
<li className="flex justify-between px-1 py-2">
|
||||||
<span className="font-bold">
|
<span className="font-bold">
|
||||||
{intl.formatMessage(messages.profilechanged)}
|
{intl.formatMessage(messages.profilechanged)}
|
||||||
</span>
|
</span>
|
||||||
<span>ID {profile}</span>
|
<span>{profile}</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{rootFolder && (
|
{rootFolder && (
|
||||||
@@ -239,6 +241,14 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
|
|||||||
<span>{rootFolder}</span>
|
<span>{rootFolder}</span>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ const RequestCard: React.FC<RequestCardProps> = ({ request, onTitleData }) => {
|
|||||||
<img
|
<img
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm"
|
className="avatar-sm object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
||||||
{requestData.requestedBy.displayName}
|
{requestData.requestedBy.displayName}
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={requestData.requestedBy.avatar}
|
src={requestData.requestedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm ml-1.5"
|
className="avatar-sm ml-1.5 object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||||
{requestData.requestedBy.displayName}
|
{requestData.requestedBy.displayName}
|
||||||
@@ -390,7 +390,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={requestData.modifiedBy.avatar}
|
src={requestData.modifiedBy.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="avatar-sm ml-1.5"
|
className="avatar-sm ml-1.5 object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||||
{requestData.modifiedBy.displayName}
|
{requestData.modifiedBy.displayName}
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={selectedUser.avatar}
|
src={selectedUser.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="ml-3 block">
|
<span className="ml-3 block">
|
||||||
{selectedUser.displayName}
|
{selectedUser.displayName}
|
||||||
@@ -584,7 +584,7 @@ const AdvancedRequester: React.FC<AdvancedRequesterProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-6 w-6 flex-shrink-0 rounded-full"
|
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="ml-3 block flex-shrink-0">
|
<span className="ml-3 block flex-shrink-0">
|
||||||
{user.displayName}
|
{user.displayName}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const messages = defineMessages({
|
|||||||
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||||
validationSmtpPortRequired: 'You must provide a valid port number',
|
validationSmtpPortRequired: 'You must provide a valid port number',
|
||||||
agentenabled: 'Enable Agent',
|
agentenabled: 'Enable Agent',
|
||||||
|
userEmailRequired: 'Require user email',
|
||||||
emailsender: 'Sender Address',
|
emailsender: 'Sender Address',
|
||||||
smtpHost: 'SMTP Host',
|
smtpHost: 'SMTP Host',
|
||||||
smtpPort: 'SMTP Port',
|
smtpPort: 'SMTP Port',
|
||||||
@@ -125,6 +126,7 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
|
userEmailRequired: data.options.userEmailRequired,
|
||||||
emailFrom: data.options.emailFrom,
|
emailFrom: data.options.emailFrom,
|
||||||
smtpHost: data.options.smtpHost,
|
smtpHost: data.options.smtpHost,
|
||||||
smtpPort: data.options.smtpPort ?? 587,
|
smtpPort: data.options.smtpPort ?? 587,
|
||||||
@@ -148,6 +150,7 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
await axios.post('/api/v1/settings/notifications/email', {
|
await axios.post('/api/v1/settings/notifications/email', {
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
options: {
|
options: {
|
||||||
|
userEmailRequired: values.userEmailRequired,
|
||||||
emailFrom: values.emailFrom,
|
emailFrom: values.emailFrom,
|
||||||
smtpHost: values.smtpHost,
|
smtpHost: values.smtpHost,
|
||||||
smtpPort: Number(values.smtpPort),
|
smtpPort: Number(values.smtpPort),
|
||||||
@@ -241,6 +244,18 @@ const NotificationsEmail: React.FC = () => {
|
|||||||
<Field type="checkbox" id="enabled" name="enabled" />
|
<Field type="checkbox" id="enabled" name="enabled" />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="form-row">
|
||||||
<label htmlFor="senderName" className="text-label">
|
<label htmlFor="senderName" className="text-label">
|
||||||
{intl.formatMessage(messages.senderName)}
|
{intl.formatMessage(messages.senderName)}
|
||||||
|
|||||||
@@ -188,35 +188,6 @@ const SettingsAbout: React.FC = () => {
|
|||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</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">
|
<div className="section">
|
||||||
<Releases currentVersion={data.version} />
|
<Releases currentVersion={data.version} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,30 +12,31 @@ import Badge from '../Common/Badge';
|
|||||||
import Button from '../Common/Button';
|
import Button from '../Common/Button';
|
||||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||||
import LibraryItem from './LibraryItem';
|
import LibraryItem from './LibraryItem';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
jellyfinsettings: 'Jellyfin Settings',
|
jellyfinsettings: '{mediaServerName} Settings',
|
||||||
jellyfinsettingsDescription:
|
jellyfinsettingsDescription:
|
||||||
'Configure the settings for your Jellyfin server. Jellyfin scans your Jellyfin libraries to see what content is available.',
|
'Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.',
|
||||||
timeout: 'Timeout',
|
timeout: 'Timeout',
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
saving: 'Saving…',
|
saving: 'Saving…',
|
||||||
jellyfinlibraries: 'Jellyfin Libraries',
|
jellyfinlibraries: '{mediaServerName} Libraries',
|
||||||
jellyfinlibrariesDescription:
|
jellyfinlibrariesDescription:
|
||||||
'The libraries Jellyfin scans for titles. Click the button below if no libraries are listed.',
|
'The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.',
|
||||||
jellyfinSettingsFailure:
|
jellyfinSettingsFailure:
|
||||||
'Something went wrong while saving Jellyfin settings.',
|
'Something went wrong while saving {mediaServerName} settings.',
|
||||||
jellyfinSettingsSuccess: 'Jellyfin settings saved successfully!',
|
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
||||||
jellyfinSettings: 'Jellyfin Settings',
|
jellyfinSettings: '{mediaServerName} Settings',
|
||||||
jellyfinSettingsDescription:
|
jellyfinSettingsDescription:
|
||||||
'Optionally configure an external player endpoint for your jellyfin server that is different to the internal URL used during setup',
|
'Optionally configure an external player endpoint for your {mediaServerName} server that is different to the internal URL used during setup',
|
||||||
externalUrl: 'External URL',
|
externalUrl: 'External URL',
|
||||||
validationUrl: 'You must provide a valid URL',
|
validationUrl: 'You must provide a valid URL',
|
||||||
syncing: 'Syncing',
|
syncing: 'Syncing',
|
||||||
syncJellyfin: 'Sync Libraries',
|
syncJellyfin: 'Sync Libraries',
|
||||||
manualscanJellyfin: 'Manual Library Scan',
|
manualscanJellyfin: 'Manual Library Scan',
|
||||||
manualscanDescriptionJellyfin:
|
manualscanDescriptionJellyfin:
|
||||||
"Normally, this will only be run once every 24 hours. Jellyfin will check your Jellyfin server's recently added more aggressively. If this is your first time configuring Jellyfin, a one-time full manual library scan is recommended!",
|
"Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!",
|
||||||
notrunning: 'Not Running',
|
notrunning: 'Not Running',
|
||||||
currentlibrary: 'Current Library: {name}',
|
currentlibrary: 'Current Library: {name}',
|
||||||
librariesRemaining: 'Libraries Remaining: {count}',
|
librariesRemaining: 'Libraries Remaining: {count}',
|
||||||
@@ -80,6 +81,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
);
|
);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const JellyfinSettingsSchema = Yup.object().shape({
|
const JellyfinSettingsSchema = Yup.object().shape({
|
||||||
jellyfinExternalUrl: Yup.string().matches(
|
jellyfinExternalUrl: Yup.string().matches(
|
||||||
@@ -161,10 +163,22 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="heading">
|
<h3 className="heading">
|
||||||
<FormattedMessage {...messages.jellyfinlibraries} />
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.jellyfinlibraries, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.jellyfinlibraries, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="description">
|
<p className="description">
|
||||||
<FormattedMessage {...messages.jellyfinlibrariesDescription} />
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.jellyfinlibrariesDescription, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.jellyfinlibrariesDescription, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
@@ -201,7 +215,13 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
<FormattedMessage {...messages.manualscanJellyfin} />
|
<FormattedMessage {...messages.manualscanJellyfin} />
|
||||||
</h3>
|
</h3>
|
||||||
<p className="description">
|
<p className="description">
|
||||||
<FormattedMessage {...messages.manualscanDescriptionJellyfin} />
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.manualscanDescriptionJellyfin, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
@@ -305,10 +325,22 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="mt-10 mb-6">
|
<div className="mt-10 mb-6">
|
||||||
<h3 className="heading">
|
<h3 className="heading">
|
||||||
{intl.formatMessage(messages.jellyfinSettings)}
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.jellyfinSettings, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.jellyfinSettings, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="description">
|
<p className="description">
|
||||||
{intl.formatMessage(messages.jellyfinSettingsDescription)}
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.jellyfinSettingsDescription, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Formik
|
<Formik
|
||||||
@@ -322,15 +354,31 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
externalHostname: values.jellyfinExternalUrl,
|
externalHostname: values.jellyfinExternalUrl,
|
||||||
} as JellyfinSettings);
|
} as JellyfinSettings);
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.jellyfinSettingsSuccess), {
|
addToast(
|
||||||
|
intl.formatMessage(messages.jellyfinSettingsSuccess, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
}),
|
||||||
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(intl.formatMessage(messages.jellyfinSettingsFailure), {
|
addToast(
|
||||||
|
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
|
}),
|
||||||
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
revalidate();
|
revalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import {
|
|||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { MediaServerType } from '../../../../server/constants/server';
|
||||||
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
|
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||||
import { JobId } from '../../../../server/lib/settings';
|
import { JobId } from '../../../../server/lib/settings';
|
||||||
import Spinner from '../../../assets/spinner.svg';
|
import Spinner from '../../../assets/spinner.svg';
|
||||||
|
import useSettings from '../../../hooks/useSettings';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import { formatBytes } from '../../../utils/numberHelpers';
|
import { formatBytes } from '../../../utils/numberHelpers';
|
||||||
import Badge from '../../Common/Badge';
|
import Badge from '../../Common/Badge';
|
||||||
@@ -102,6 +104,7 @@ const SettingsJobs: React.FC = () => {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5);
|
const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5);
|
||||||
const [jobScheduleHours, setJobScheduleHours] = useState(1);
|
const [jobScheduleHours, setJobScheduleHours] = useState(1);
|
||||||
|
const settings = useSettings();
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -369,7 +372,15 @@ const SettingsJobs: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<Table.TBody>
|
<Table.TBody>
|
||||||
{cacheData?.map((cache) => (
|
{cacheData
|
||||||
|
?.filter(
|
||||||
|
(cache) =>
|
||||||
|
!(
|
||||||
|
settings.currentSettings.mediaServerType !==
|
||||||
|
MediaServerType.PLEX && cache.id === 'plexguid'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((cache) => (
|
||||||
<tr key={`cache-list-${cache.id}`}>
|
<tr key={`cache-list-${cache.id}`}>
|
||||||
<Table.TD>{cache.name}</Table.TD>
|
<Table.TD>{cache.name}</Table.TD>
|
||||||
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
|
<Table.TD>{intl.formatNumber(cache.stats.hits)}</Table.TD>
|
||||||
@@ -378,7 +389,10 @@ const SettingsJobs: React.FC = () => {
|
|||||||
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
|
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
|
||||||
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
|
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
|
||||||
<Table.TD alignText="right">
|
<Table.TD alignText="right">
|
||||||
<Button buttonType="danger" onClick={() => flushCache(cache)}>
|
<Button
|
||||||
|
buttonType="danger"
|
||||||
|
onClick={() => flushCache(cache)}
|
||||||
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
<span>{intl.formatMessage(messages.flushcache)}</span>
|
<span>{intl.formatMessage(messages.flushcache)}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import getConfig from 'next/config';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { MediaServerType } from '../../../server/constants/server';
|
||||||
|
import useSettings from '../../hooks/useSettings';
|
||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
import PageTitle from '../Common/PageTitle';
|
import PageTitle from '../Common/PageTitle';
|
||||||
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
|
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
|
||||||
@@ -8,7 +11,7 @@ const messages = defineMessages({
|
|||||||
menuGeneralSettings: 'General',
|
menuGeneralSettings: 'General',
|
||||||
menuUsers: 'Users',
|
menuUsers: 'Users',
|
||||||
menuPlexSettings: 'Plex',
|
menuPlexSettings: 'Plex',
|
||||||
menuJellyfinSettings: 'Jellyfin',
|
menuJellyfinSettings: '{mediaServerName}',
|
||||||
menuServices: 'Services',
|
menuServices: 'Services',
|
||||||
menuNotifications: 'Notifications',
|
menuNotifications: 'Notifications',
|
||||||
menuLogs: 'Logs',
|
menuLogs: 'Logs',
|
||||||
@@ -18,7 +21,8 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
const SettingsLayout: React.FC = ({ children }) => {
|
const SettingsLayout: React.FC = ({ children }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
const settings = useSettings();
|
||||||
const settingsRoutes: SettingsRoute[] = [
|
const settingsRoutes: SettingsRoute[] = [
|
||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.menuGeneralSettings),
|
text: intl.formatMessage(messages.menuGeneralSettings),
|
||||||
@@ -30,13 +34,14 @@ const SettingsLayout: React.FC = ({ children }) => {
|
|||||||
route: '/settings/users',
|
route: '/settings/users',
|
||||||
regex: /^\/settings\/users/,
|
regex: /^\/settings\/users/,
|
||||||
},
|
},
|
||||||
{
|
settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
||||||
|
? {
|
||||||
text: intl.formatMessage(messages.menuPlexSettings),
|
text: intl.formatMessage(messages.menuPlexSettings),
|
||||||
route: '/settings/plex',
|
route: '/settings/plex',
|
||||||
regex: /^\/settings\/plex/,
|
regex: /^\/settings\/plex/,
|
||||||
},
|
}
|
||||||
{
|
: {
|
||||||
text: intl.formatMessage(messages.menuJellyfinSettings),
|
text: getAvailableMediaServerName(),
|
||||||
route: '/settings/jellyfin',
|
route: '/settings/jellyfin',
|
||||||
regex: /^\/settings\/jellyfin/,
|
regex: /^\/settings\/jellyfin/,
|
||||||
},
|
},
|
||||||
@@ -76,6 +81,12 @@ const SettingsLayout: React.FC = ({ children }) => {
|
|||||||
<div className="mt-10 text-white">{children}</div>
|
<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;
|
export default SettingsLayout;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner';
|
|||||||
import PageTitle from '../../Common/PageTitle';
|
import PageTitle from '../../Common/PageTitle';
|
||||||
import PermissionEdit from '../../PermissionEdit';
|
import PermissionEdit from '../../PermissionEdit';
|
||||||
import QuotaSelector from '../../QuotaSelector';
|
import QuotaSelector from '../../QuotaSelector';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
users: 'Users',
|
users: 'Users',
|
||||||
@@ -42,6 +43,7 @@ const SettingsUsers: React.FC = () => {
|
|||||||
mutate: revalidate,
|
mutate: revalidate,
|
||||||
} = useSWR<MainSettings>('/api/v1/settings/main');
|
} = useSWR<MainSettings>('/api/v1/settings/main');
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -131,7 +133,9 @@ const SettingsUsers: React.FC = () => {
|
|||||||
<label htmlFor="newPlexLogin" className="checkbox-label">
|
<label htmlFor="newPlexLogin" className="checkbox-label">
|
||||||
{intl.formatMessage(messages.newPlexLogin, {
|
{intl.formatMessage(messages.newPlexLogin, {
|
||||||
mediaServerName:
|
mediaServerName:
|
||||||
settings.currentSettings.mediaServerType ===
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? 'Plex'
|
||||||
: 'Jellyfin',
|
: 'Jellyfin',
|
||||||
@@ -139,7 +143,9 @@ const SettingsUsers: React.FC = () => {
|
|||||||
<span className="label-tip">
|
<span className="label-tip">
|
||||||
{intl.formatMessage(messages.newPlexLoginTip, {
|
{intl.formatMessage(messages.newPlexLoginTip, {
|
||||||
mediaServerName:
|
mediaServerName:
|
||||||
settings.currentSettings.mediaServerType ===
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? 'Plex'
|
||||||
: 'Jellyfin',
|
: 'Jellyfin',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useUser } from '../../hooks/useUser';
|
|||||||
import PlexLoginButton from '../PlexLoginButton';
|
import PlexLoginButton from '../PlexLoginButton';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
welcome: 'Welcome to Overseerr',
|
welcome: 'Welcome to Jellyseerr',
|
||||||
signinMessage: 'Get started by signing in with your Plex account',
|
signinMessage: 'Get started by signing in with your Plex account',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { useUser } from '../../hooks/useUser';
|
|||||||
import PlexLoginButton from '../PlexLoginButton';
|
import PlexLoginButton from '../PlexLoginButton';
|
||||||
import JellyfinLogin from '../Login/JellyfinLogin';
|
import JellyfinLogin from '../Login/JellyfinLogin';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import Accordion from '../Common/Accordion';
|
import Accordion from '../Common/Accordion';
|
||||||
import { MediaServerType } from '../../../server/constants/server';
|
import { MediaServerType } from '../../../server/constants/server';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
welcome: 'Welcome to Overseerr',
|
welcome: 'Welcome to Jellyseerr',
|
||||||
signinMessage: 'Get started by signing in',
|
signinMessage: 'Get started by signing in',
|
||||||
signinWithJellyfin: 'Use your Jellyfin account',
|
signinWithJellyfin: 'Use your {mediaServerName} account',
|
||||||
signinWithPlex: 'Use your Plex account',
|
signinWithPlex: 'Use your Plex account',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +25,8 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
|
|||||||
MediaServerType.NOT_CONFIGURED
|
MediaServerType.NOT_CONFIGURED
|
||||||
);
|
);
|
||||||
const { user, revalidate } = useUser();
|
const { user, revalidate } = useUser();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
|
// 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
|
// 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.
|
// ask swr to revalidate the user which _shouid_ come back with a valid user.
|
||||||
@@ -91,7 +93,13 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => handleClick(1)}
|
onClick={() => handleClick(1)}
|
||||||
>
|
>
|
||||||
<FormattedMessage {...messages.signinWithJellyfin} />
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? intl.formatMessage(messages.signinWithJellyfin, {
|
||||||
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.signinWithJellyfin, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
</button>
|
</button>
|
||||||
<AccordionContent isOpen={openIndexes.includes(1)}>
|
<AccordionContent isOpen={openIndexes.includes(1)}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import RequestButton from '../RequestButton';
|
|||||||
import RequestModal from '../RequestModal';
|
import RequestModal from '../RequestModal';
|
||||||
import Slider from '../Slider';
|
import Slider from '../Slider';
|
||||||
import StatusBadge from '../StatusBadge';
|
import StatusBadge from '../StatusBadge';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
firstAirDate: 'First Air Date',
|
firstAirDate: 'First Air Date',
|
||||||
@@ -85,6 +86,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
router.query.manage == '1' ? true : false
|
router.query.manage == '1' ? true : false
|
||||||
);
|
);
|
||||||
const [showIssueModal, setShowIssueModal] = useState(false);
|
const [showIssueModal, setShowIssueModal] = useState(false);
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -124,10 +126,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text:
|
text: getAvalaibleMediaServerName(),
|
||||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' })
|
|
||||||
: intl.formatMessage(messages.play, { mediaServerName: 'Plex' }),
|
|
||||||
url: data.mediaInfo?.mediaUrl,
|
url: data.mediaInfo?.mediaUrl,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -141,10 +140,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
mediaLinks.push({
|
mediaLinks.push({
|
||||||
text:
|
text: getAvalaible4kMediaServerName(),
|
||||||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' })
|
|
||||||
: intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }),
|
|
||||||
url: data.mediaInfo?.mediaUrl4k,
|
url: data.mediaInfo?.mediaUrl4k,
|
||||||
svg: <PlayIcon />,
|
svg: <PlayIcon />,
|
||||||
});
|
});
|
||||||
@@ -228,6 +224,30 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
|
|||||||
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
|
||||||
?.flatrate ?? [];
|
?.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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="media-page"
|
className="media-page"
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import useSettings from '../../hooks/useSettings';
|
|||||||
import globalMessages from '../../i18n/globalMessages';
|
import globalMessages from '../../i18n/globalMessages';
|
||||||
import Alert from '../Common/Alert';
|
import Alert from '../Common/Alert';
|
||||||
import Modal from '../Common/Modal';
|
import Modal from '../Common/Modal';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
|
||||||
|
|
||||||
interface JellyfinImportProps {
|
interface JellyfinImportProps {
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@@ -15,23 +17,25 @@ interface JellyfinImportProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
importfromJellyfin: 'Import Jellyfin Users',
|
importfromJellyfin: 'Import {mediaServerName} Users',
|
||||||
importfromJellyfinerror:
|
importfromJellyfinerror:
|
||||||
'Something went wrong while importing Jellyfin users.',
|
'Something went wrong while importing {mediaServerName} users.',
|
||||||
importedfromJellyfin:
|
importedfromJellyfin:
|
||||||
'<strong>{userCount}</strong> Jellyfin {userCount, plural, one {user} other {users}} imported successfully!',
|
'<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
noJellyfinuserstoimport: 'There are no Jellyfin users to import.',
|
noJellyfinuserstoimport: 'There are no {mediaServerName} users to import.',
|
||||||
newJellyfinsigninenabled:
|
newJellyfinsigninenabled:
|
||||||
'The <strong>Enable New Jellyfin Sign-In</strong> setting is currently enabled. Jellyfin users with library access do not need to be imported in order to sign in.',
|
'The <strong>Enable New {mediaServerName} Sign-In</strong> setting is currently enabled. {mediaServerName} users with library access do not need to be imported in order to sign in.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
onComplete,
|
onComplete,
|
||||||
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const [isImporting, setImporting] = useState(false);
|
const [isImporting, setImporting] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
@@ -47,6 +51,18 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
revalidateOnMount: true,
|
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 () => {
|
const importUsers = async () => {
|
||||||
setImporting(true);
|
setImporting(true);
|
||||||
|
|
||||||
@@ -66,6 +82,8 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
strong: function strong(msg) {
|
strong: function strong(msg) {
|
||||||
return <strong>{msg}</strong>;
|
return <strong>{msg}</strong>;
|
||||||
},
|
},
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
@@ -77,10 +95,16 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(intl.formatMessage(messages.importfromJellyfinerror), {
|
addToast(
|
||||||
|
intl.formatMessage(messages.importfromJellyfinerror, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
|
}),
|
||||||
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setImporting(false);
|
setImporting(false);
|
||||||
}
|
}
|
||||||
@@ -110,7 +134,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
loading={!data && !error}
|
loading={!data && !error}
|
||||||
title={intl.formatMessage(messages.importfromJellyfin)}
|
title={intl.formatMessage(messages.importfromJellyfin, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
|
})}
|
||||||
iconSvg={<InboxInIcon />}
|
iconSvg={<InboxInIcon />}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
importUsers();
|
importUsers();
|
||||||
@@ -126,6 +153,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
{settings.currentSettings.newPlexLogin && (
|
{settings.currentSettings.newPlexLogin && (
|
||||||
<Alert
|
<Alert
|
||||||
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
|
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
|
? 'Emby'
|
||||||
|
: 'Jellyfin',
|
||||||
strong: function strong(msg) {
|
strong: function strong(msg) {
|
||||||
return (
|
return (
|
||||||
<strong className="font-semibold text-white">{msg}</strong>
|
<strong className="font-semibold text-white">{msg}</strong>
|
||||||
@@ -240,7 +271,10 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Alert
|
<Alert
|
||||||
title={intl.formatMessage(messages.noJellyfinuserstoimport)}
|
title={intl.formatMessage(messages.noJellyfinuserstoimport, {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
|
})}
|
||||||
type="info"
|
type="info"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@heroicons/react/solid';
|
} from '@heroicons/react/solid';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import getConfig from 'next/config';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -40,7 +41,7 @@ import PlexImportModal from './PlexImportModal';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
users: 'Users',
|
users: 'Users',
|
||||||
userlist: 'User List',
|
userlist: 'User List',
|
||||||
importfromplex: 'Import {mediaServerName} Users',
|
importfrommediaserver: 'Import {mediaServerName} Users',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
totalrequests: 'Requests',
|
totalrequests: 'Requests',
|
||||||
accounttype: 'Type',
|
accounttype: 'Type',
|
||||||
@@ -87,6 +88,7 @@ const UserList: React.FC = () => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
const [currentSort, setCurrentSort] = useState<Sort>('displayname');
|
||||||
@@ -480,7 +482,9 @@ const UserList: React.FC = () => {
|
|||||||
setShowImportModal(false);
|
setShowImportModal(false);
|
||||||
revalidate();
|
revalidate();
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{data.pageInfo.results}
|
||||||
|
</JellyfinImportModal>
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
@@ -503,12 +507,17 @@ const UserList: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<InboxInIcon />
|
<InboxInIcon />
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.importfromplex, {
|
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||||
mediaServerName:
|
? intl.formatMessage(messages.importfrommediaserver, {
|
||||||
settings.currentSettings.mediaServerType ===
|
mediaServerName: 'Emby',
|
||||||
|
})
|
||||||
|
: settings.currentSettings.mediaServerType ===
|
||||||
MediaServerType.PLEX
|
MediaServerType.PLEX
|
||||||
? 'Plex'
|
? intl.formatMessage(messages.importfrommediaserver, {
|
||||||
: 'Jellyfin',
|
mediaServerName: 'Plex',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.importfrommediaserver, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -596,7 +605,7 @@ const UserList: React.FC = () => {
|
|||||||
<Link href={`/users/${user.id}`}>
|
<Link href={`/users/${user.id}`}>
|
||||||
<a className="h-10 w-10 flex-shrink-0">
|
<a className="h-10 w-10 flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
className="h-10 w-10 rounded-full"
|
className="h-10 w-10 rounded-full object-cover"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
@@ -636,17 +645,23 @@ const UserList: React.FC = () => {
|
|||||||
<Badge badgeType="warning">
|
<Badge badgeType="warning">
|
||||||
{intl.formatMessage(messages.plexuser)}
|
{intl.formatMessage(messages.plexuser)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : user.userType === UserType.LOCAL ? (
|
||||||
<Badge badgeType="default">
|
<Badge badgeType="default">
|
||||||
|
{intl.formatMessage(messages.localuser)}
|
||||||
|
</Badge>
|
||||||
|
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
|
||||||
|
<Badge badgeType="success">
|
||||||
{intl.formatMessage(messages.mediaServerUser, {
|
{intl.formatMessage(messages.mediaServerUser, {
|
||||||
mediaServerName:
|
mediaServerName: 'Emby',
|
||||||
settings.currentSettings.mediaServerType ===
|
|
||||||
MediaServerType.PLEX
|
|
||||||
? 'Plex'
|
|
||||||
: 'Jellyfin',
|
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : user.userType === UserType.JELLYFIN ? (
|
||||||
|
<Badge badgeType="default">
|
||||||
|
{intl.formatMessage(messages.mediaServerUser, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
{user.id === 1
|
{user.id === 1
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
className="h-24 w-24 rounded-full bg-gray-600 ring-1 ring-gray-700"
|
className="h-24 w-24 rounded-full bg-gray-600 object-cover ring-1 ring-gray-700"
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { MediaServerType } from '../../../../../server/constants/server';
|
|
||||||
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||||
import {
|
import {
|
||||||
availableLanguages,
|
availableLanguages,
|
||||||
@@ -25,11 +24,13 @@ import PageTitle from '../../../Common/PageTitle';
|
|||||||
import LanguageSelector from '../../../LanguageSelector';
|
import LanguageSelector from '../../../LanguageSelector';
|
||||||
import QuotaSelector from '../../../QuotaSelector';
|
import QuotaSelector from '../../../QuotaSelector';
|
||||||
import RegionSelector from '../../../RegionSelector';
|
import RegionSelector from '../../../RegionSelector';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
general: 'General',
|
general: 'General',
|
||||||
generalsettings: 'General Settings',
|
generalsettings: 'General Settings',
|
||||||
displayName: 'Display Name',
|
displayName: 'Display Name',
|
||||||
|
email: 'Email',
|
||||||
save: 'Save Changes',
|
save: 'Save Changes',
|
||||||
saving: 'Saving…',
|
saving: 'Saving…',
|
||||||
mediaServerUser: '{mediaServerName} User',
|
mediaServerUser: '{mediaServerName} User',
|
||||||
@@ -59,7 +60,7 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
const UserGeneralSettings: React.FC = () => {
|
const UserGeneralSettings: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const settings = useSettings();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const { locale, setLocale } = useLocale();
|
const { locale, setLocale } = useLocale();
|
||||||
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
|
const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false);
|
||||||
@@ -120,8 +121,9 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
displayName: data?.username,
|
displayName: data?.username ?? '',
|
||||||
discordId: data?.discordId,
|
email: data?.email ?? '',
|
||||||
|
discordId: data?.discordId ?? '',
|
||||||
locale: data?.locale,
|
locale: data?.locale,
|
||||||
region: data?.region,
|
region: data?.region,
|
||||||
originalLanguage: data?.originalLanguage,
|
originalLanguage: data?.originalLanguage,
|
||||||
@@ -136,6 +138,7 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
|
await axios.post(`/api/v1/user/${user?.id}/settings/main`, {
|
||||||
username: values.displayName,
|
username: values.displayName,
|
||||||
|
email: values.email,
|
||||||
discordId: values.discordId,
|
discordId: values.discordId,
|
||||||
locale: values.locale,
|
locale: values.locale,
|
||||||
region: values.region,
|
region: values.region,
|
||||||
@@ -189,19 +192,25 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
<div className="flex max-w-lg items-center">
|
<div className="flex max-w-lg items-center">
|
||||||
{user?.userType === UserType.PLEX ? (
|
{user?.userType === UserType.PLEX ? (
|
||||||
<Badge badgeType="warning">
|
<Badge badgeType="warning">
|
||||||
|
{intl.formatMessage(messages.plexuser)}
|
||||||
|
</Badge>
|
||||||
|
) : user?.userType === UserType.LOCAL ? (
|
||||||
|
<Badge badgeType="default">
|
||||||
{intl.formatMessage(messages.localuser)}
|
{intl.formatMessage(messages.localuser)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
|
||||||
<Badge badgeType="default">
|
<Badge badgeType="success">
|
||||||
{intl.formatMessage(messages.mediaServerUser, {
|
{intl.formatMessage(messages.mediaServerUser, {
|
||||||
mediaServerName:
|
mediaServerName: 'Emby',
|
||||||
settings.currentSettings.mediaServerType ===
|
|
||||||
MediaServerType.PLEX
|
|
||||||
? 'Plex'
|
|
||||||
: 'Jellyfin',
|
|
||||||
})}
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : user?.userType === UserType.JELLYFIN ? (
|
||||||
|
<Badge badgeType="default">
|
||||||
|
{intl.formatMessage(messages.mediaServerUser, {
|
||||||
|
mediaServerName: 'Jellyfin',
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,6 +248,32 @@ const UserGeneralSettings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="form-row">
|
||||||
<label htmlFor="discordId" className="text-label">
|
<label htmlFor="discordId" className="text-label">
|
||||||
{intl.formatMessage(messages.discordId)}
|
{intl.formatMessage(messages.discordId)}
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ const useDiscover = <T extends BaseMedia, S = Record<string, never>>(
|
|||||||
|
|
||||||
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
||||||
const isReachingEnd =
|
const isReachingEnd =
|
||||||
isEmpty || (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20);
|
isEmpty ||
|
||||||
|
(!!data && (data[data?.length - 1]?.results.length ?? 0) < 20) ||
|
||||||
|
(!!data && (data[data?.length - 1]?.totalResults ?? 0) < 41);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoadingInitialData,
|
isLoadingInitialData,
|
||||||
|
|||||||
@@ -1,45 +1,61 @@
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { MediaRequest } from '../../server/entity/MediaRequest';
|
import { MediaRequest } from '../../server/entity/MediaRequest';
|
||||||
import { ServiceCommonServer } from '../../server/interfaces/api/serviceInterfaces';
|
import {
|
||||||
|
ServiceCommonServer,
|
||||||
|
ServiceCommonServerWithDetails,
|
||||||
|
} from '../../server/interfaces/api/serviceInterfaces';
|
||||||
|
|
||||||
interface OverrideStatus {
|
interface OverrideStatus {
|
||||||
server: string | null;
|
server?: string;
|
||||||
profile: number | null;
|
profile?: string;
|
||||||
rootFolder: string | null;
|
rootFolder?: string;
|
||||||
|
languageProfile?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRequestOverride = (request: MediaRequest): OverrideStatus => {
|
const useRequestOverride = (request: MediaRequest): OverrideStatus => {
|
||||||
const { data } = useSWR<ServiceCommonServer[]>(
|
const { data: allServers } = useSWR<ServiceCommonServer[]>(
|
||||||
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}`
|
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
const { data } = useSWR<ServiceCommonServerWithDetails>(
|
||||||
return {
|
`/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}/${
|
||||||
server: null,
|
request.serverId
|
||||||
profile: null,
|
}`
|
||||||
rootFolder: null,
|
);
|
||||||
};
|
|
||||||
|
if (!data || !allServers) {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultServer = data.find(
|
const defaultServer = allServers.find(
|
||||||
(server) => server.is4k === request.is4k && server.isDefault
|
(server) => server.is4k === request.is4k && server.isDefault
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeServer = data.find((server) => server.id === request.serverId);
|
const activeServer = allServers.find(
|
||||||
|
(server) => server.id === request.serverId
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server:
|
server:
|
||||||
activeServer && request.serverId !== defaultServer?.id
|
activeServer && request.serverId !== defaultServer?.id
|
||||||
? activeServer.name
|
? activeServer.name
|
||||||
: null,
|
: undefined,
|
||||||
profile:
|
profile:
|
||||||
defaultServer?.activeProfileId !== request.profileId
|
defaultServer?.activeProfileId !== request.profileId
|
||||||
? request.profileId
|
? data.profiles.find((profile) => profile.id === request.profileId)
|
||||||
: null,
|
?.name
|
||||||
|
: undefined,
|
||||||
rootFolder:
|
rootFolder:
|
||||||
defaultServer?.activeDirectory !== request.rootFolder
|
defaultServer?.activeDirectory !== request.rootFolder
|
||||||
? request.rootFolder
|
? request.rootFolder
|
||||||
: null,
|
: undefined,
|
||||||
|
languageProfile:
|
||||||
|
request.type === 'tv' &&
|
||||||
|
defaultServer?.activeLanguageProfileId !== request.languageProfileId
|
||||||
|
? data.languageProfiles?.find(
|
||||||
|
(profile) => profile.id === request.languageProfileId
|
||||||
|
)?.name
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import type { UrlObject } from 'url';
|
|
||||||
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
|
|
||||||
import useDebouncedState from './useDebouncedState';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
import type { UrlObject } from 'url';
|
||||||
import type { Nullable } from '../utils/typeHelpers';
|
import type { Nullable } from '../utils/typeHelpers';
|
||||||
|
import useDebouncedState from './useDebouncedState';
|
||||||
|
|
||||||
type Url = string | UrlObject;
|
type Url = string | UrlObject;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const useSearchInput = (): SearchObject => {
|
|||||||
* in a new route. If we are, then we only replace the history.
|
* in a new route. If we are, then we only replace the history.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedValue !== '') {
|
if (debouncedValue !== '' && searchOpen) {
|
||||||
if (router.pathname.startsWith('/search')) {
|
if (router.pathname.startsWith('/search')) {
|
||||||
router.replace({
|
router.replace({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type { PermissionCheckOptions };
|
|||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
warnings: string[];
|
||||||
plexUsername?: string;
|
plexUsername?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|||||||
@@ -686,7 +686,8 @@
|
|||||||
"components.UserList.nouserstoimport": "No hi ha usuaris nous de Plex a importar.",
|
"components.UserList.nouserstoimport": "No hi ha usuaris nous de Plex a importar.",
|
||||||
"components.UserList.localuser": "Usuari local",
|
"components.UserList.localuser": "Usuari local",
|
||||||
"components.UserList.importfromplexerror": "S'ha produït un error en importar usuaris de Plex.",
|
"components.UserList.importfromplexerror": "S'ha produït un error en importar usuaris de Plex.",
|
||||||
"components.UserList.importfromplex": "Importeu usuaris de {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importeu usuaris de {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importeu usuaris de Plex",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuari} other {usuaris}} de Plex importat correctament!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuari} other {usuaris}} de Plex importat correctament!",
|
||||||
"components.TvDetails.watchtrailer": "Veure el tràiler",
|
"components.TvDetails.watchtrailer": "Veure el tràiler",
|
||||||
"components.TvDetails.viewfullcrew": "Mostreu equip complet",
|
"components.TvDetails.viewfullcrew": "Mostreu equip complet",
|
||||||
|
|||||||
@@ -850,7 +850,8 @@
|
|||||||
"components.UserList.nouserstoimport": "Ingen nye brugere som kan importeres fra Plex.",
|
"components.UserList.nouserstoimport": "Ingen nye brugere som kan importeres fra Plex.",
|
||||||
"components.UserList.edituser": "Redigér Brugertilladelser",
|
"components.UserList.edituser": "Redigér Brugertilladelser",
|
||||||
"components.UserList.email": "Email Adresse",
|
"components.UserList.email": "Email Adresse",
|
||||||
"components.UserList.importfromplex": "Importér Brugere fra {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importér Brugere fra {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importér Brugere fra Plex",
|
||||||
"components.UserList.owner": "Ejer",
|
"components.UserList.owner": "Ejer",
|
||||||
"components.UserList.password": "Kodeord",
|
"components.UserList.password": "Kodeord",
|
||||||
"components.UserList.passwordinfodescription": "Konfigurér en applikations-URL og aktivér emailnotifikationer for at tillade automatisk kodeordsgenerering.",
|
"components.UserList.passwordinfodescription": "Konfigurér en applikations-URL og aktivér emailnotifikationer for at tillade automatisk kodeordsgenerering.",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@
|
|||||||
"components.Settings.SettingsAbout.Releases.latestversion": "Neuste",
|
"components.Settings.SettingsAbout.Releases.latestversion": "Neuste",
|
||||||
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
|
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
|
||||||
"components.UserList.importfromplexerror": "Beim Importieren von Plex-Benutzern ist etwas schief gelaufen.",
|
"components.UserList.importfromplexerror": "Beim Importieren von Plex-Benutzern ist etwas schief gelaufen.",
|
||||||
"components.UserList.importfromplex": "{mediaServerName}-Benutzer importieren",
|
"components.UserList.importfrommediaserver": "{mediaServerName}-Benutzer importieren",
|
||||||
|
"components.UserList.importfromplex": "Plex-Benutzer importieren",
|
||||||
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
|
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
|
||||||
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien-Crew",
|
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien-Crew",
|
||||||
"components.PersonDetails.crewmember": "Crew",
|
"components.PersonDetails.crewmember": "Crew",
|
||||||
|
|||||||
@@ -602,7 +602,8 @@
|
|||||||
"components.UserList.localuser": "Τοπικός χρήστης",
|
"components.UserList.localuser": "Τοπικός χρήστης",
|
||||||
"components.UserList.localLoginDisabled": "Η ρύθμιση <strong>Ενεργοποίηση τοπικής σύνδεσης</strong> είναι προς το παρόν απενεργοποιημένη.",
|
"components.UserList.localLoginDisabled": "Η ρύθμιση <strong>Ενεργοποίηση τοπικής σύνδεσης</strong> είναι προς το παρόν απενεργοποιημένη.",
|
||||||
"components.UserList.importfromplexerror": "Κάτι πήγε στραβά κατά την εισαγωγή χρηστών από το Plex.",
|
"components.UserList.importfromplexerror": "Κάτι πήγε στραβά κατά την εισαγωγή χρηστών από το Plex.",
|
||||||
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Εισαγωγή χρηστών από το {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Εισαγωγή χρηστών από το Plex",
|
||||||
"components.UserList.importedfromplex": "{userCount, plural, one {# νέου χρήστη} other {#νέοι χρήστες}} εισήχθησαν απο το Plex επιτυχώς!",
|
"components.UserList.importedfromplex": "{userCount, plural, one {# νέου χρήστη} other {#νέοι χρήστες}} εισήχθησαν απο το Plex επιτυχώς!",
|
||||||
"components.UserList.email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
|
"components.UserList.email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
|
||||||
"components.UserList.edituser": "Επεξεργασία δικαιωμάτων χρήστη",
|
"components.UserList.edituser": "Επεξεργασία δικαιωμάτων χρήστη",
|
||||||
|
|||||||
@@ -269,6 +269,7 @@
|
|||||||
"components.QuotaSelector.unlimited": "Unlimited",
|
"components.QuotaSelector.unlimited": "Unlimited",
|
||||||
"components.RegionSelector.regionDefault": "All Regions",
|
"components.RegionSelector.regionDefault": "All Regions",
|
||||||
"components.RegionSelector.regionServerDefault": "Default ({region})",
|
"components.RegionSelector.regionServerDefault": "Default ({region})",
|
||||||
|
"components.RequestBlock.languageprofile": "Language Profile",
|
||||||
"components.RequestBlock.profilechanged": "Quality Profile",
|
"components.RequestBlock.profilechanged": "Quality Profile",
|
||||||
"components.RequestBlock.requestoverrides": "Request Overrides",
|
"components.RequestBlock.requestoverrides": "Request Overrides",
|
||||||
"components.RequestBlock.rootfolder": "Root Folder",
|
"components.RequestBlock.rootfolder": "Root Folder",
|
||||||
@@ -860,7 +861,8 @@
|
|||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
"components.UserList.importfromplex": "Import {mediaServerName} Users",
|
"components.UserList.importfrommediaserver": "Import {mediaServerName} Users",
|
||||||
|
"components.UserList.importfromplex": "Import Plex Users",
|
||||||
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
|
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
|
||||||
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
||||||
"components.UserList.localuser": "Local User",
|
"components.UserList.localuser": "Local User",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@
|
|||||||
"components.Settings.SettingsAbout.Releases.currentversion": "Actual",
|
"components.Settings.SettingsAbout.Releases.currentversion": "Actual",
|
||||||
"components.MovieDetails.studio": "{studioCount, plural, one {Estudio} other {Estudios}}",
|
"components.MovieDetails.studio": "{studioCount, plural, one {Estudio} other {Estudios}}",
|
||||||
"components.UserList.importfromplexerror": "Algo salió mal importando usuarios de Plex.",
|
"components.UserList.importfromplexerror": "Algo salió mal importando usuarios de Plex.",
|
||||||
"components.UserList.importfromplex": "Importar Usuarios de {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importar Usuarios de {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importar Usuarios de Plex",
|
||||||
"components.UserList.importedfromplex": "¡{userCount, plural, one {# nuevo usuario} other {# nuevos usuarios}} importado/s de Plex con éxito!",
|
"components.UserList.importedfromplex": "¡{userCount, plural, one {# nuevo usuario} other {# nuevos usuarios}} importado/s de Plex con éxito!",
|
||||||
"components.TvDetails.viewfullcrew": "Ver Equipo Completo",
|
"components.TvDetails.viewfullcrew": "Ver Equipo Completo",
|
||||||
"components.TvDetails.firstAirDate": "Primera fecha de emisión",
|
"components.TvDetails.firstAirDate": "Primera fecha de emisión",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@
|
|||||||
"components.Settings.SettingsAbout.Releases.latestversion": "Dernière version",
|
"components.Settings.SettingsAbout.Releases.latestversion": "Dernière version",
|
||||||
"components.Settings.SettingsAbout.Releases.currentversion": "Actuelle",
|
"components.Settings.SettingsAbout.Releases.currentversion": "Actuelle",
|
||||||
"components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.",
|
"components.UserList.importfromplexerror": "Une erreur s'est produite durant l'importation des utilisateurs de Plex.",
|
||||||
"components.UserList.importfromplex": "Importer les utilisateurs de {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importer les utilisateurs de {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importer les utilisateurs de Plex",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utilisateur} other {utilisateurs}} importé(s) depuis Plex avec succès !",
|
"components.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.viewfullcrew": "Voir l'équipe complète de la série",
|
||||||
"components.TvDetails.TvCrew.fullseriescrew": "Équipe complète de la série",
|
"components.TvDetails.TvCrew.fullseriescrew": "Équipe complète de la série",
|
||||||
|
|||||||
@@ -165,7 +165,8 @@
|
|||||||
"components.UserList.password": "Jelszó",
|
"components.UserList.password": "Jelszó",
|
||||||
"components.UserList.localuser": "Helyi felhasználó",
|
"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.importfromplexerror": "Hiba történt a felhasználók Plex-ről történő importálása közben.",
|
||||||
"components.UserList.importfromplex": "Felhasználók importálása {mediaServerName}-ről",
|
"components.UserList.importfrommediaserver": "Felhasználók importálása {mediaServerName}-ről",
|
||||||
|
"components.UserList.importfromplex": "Felhasználók importálása Plex-ről",
|
||||||
"components.UserList.importedfromplex": "{userCount, plural, =0 {Nem lett új} one {# új} other {# új}} felhasználó importálva Plex-ről!",
|
"components.UserList.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.email": "E-mail-cím",
|
||||||
"components.UserList.deleteuser": "Felhasználó törlése",
|
"components.UserList.deleteuser": "Felhasználó törlése",
|
||||||
|
|||||||
@@ -223,7 +223,8 @@
|
|||||||
"components.Settings.SettingsAbout.Releases.latestversion": "Versione più recente",
|
"components.Settings.SettingsAbout.Releases.latestversion": "Versione più recente",
|
||||||
"components.Settings.SettingsAbout.Releases.currentversion": "Versione attuale",
|
"components.Settings.SettingsAbout.Releases.currentversion": "Versione attuale",
|
||||||
"components.UserList.importfromplexerror": "Qualcosa è andato storto nell'importare gli utenti Plex.",
|
"components.UserList.importfromplexerror": "Qualcosa è andato storto nell'importare gli utenti Plex.",
|
||||||
"components.UserList.importfromplex": "Importa utenti {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importa utenti {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importa utenti Plex",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {utente} other {utenti}} Plex {userCount, plural, one {importato} other {importati}} correttamente!",
|
"components.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.viewfullcrew": "Vedi troupe completa",
|
||||||
"components.TvDetails.TvCrew.fullseriescrew": "Troupe completa serie",
|
"components.TvDetails.TvCrew.fullseriescrew": "Troupe completa serie",
|
||||||
|
|||||||
@@ -231,7 +231,8 @@
|
|||||||
"components.TvDetails.watchtrailer": "予告編を見る",
|
"components.TvDetails.watchtrailer": "予告編を見る",
|
||||||
"components.MovieDetails.watchtrailer": "予告編を見る",
|
"components.MovieDetails.watchtrailer": "予告編を見る",
|
||||||
"components.UserList.importfromplexerror": "Plexからユーザーをインポート中に問題が発生しました。",
|
"components.UserList.importfromplexerror": "Plexからユーザーをインポート中に問題が発生しました。",
|
||||||
"components.UserList.importfromplex": "{mediaServerName}からユーザーをインポート",
|
"components.UserList.importfrommediaserver": "{mediaServerName}からユーザーをインポート",
|
||||||
|
"components.UserList.importfromplex": "Plexからユーザーをインポート",
|
||||||
"components.UserList.importedfromplex": "Plex から新ユーザー {userCount} 名をインポートしました。",
|
"components.UserList.importedfromplex": "Plex から新ユーザー {userCount} 名をインポートしました。",
|
||||||
"components.TvDetails.viewfullcrew": "フルクルーを表示",
|
"components.TvDetails.viewfullcrew": "フルクルーを表示",
|
||||||
"components.TvDetails.firstAirDate": "初放送日",
|
"components.TvDetails.firstAirDate": "初放送日",
|
||||||
|
|||||||
@@ -194,7 +194,8 @@
|
|||||||
"components.UserList.userssaved": "Brukertillatelsene ble lagret!",
|
"components.UserList.userssaved": "Brukertillatelsene ble lagret!",
|
||||||
"components.UserList.users": "Brukere",
|
"components.UserList.users": "Brukere",
|
||||||
"components.UserList.importfromplexerror": "Noe gikk galt ved importering av brukere fra Plex.",
|
"components.UserList.importfromplexerror": "Noe gikk galt ved importering av brukere fra Plex.",
|
||||||
"components.UserList.importfromplex": "Importer brukere fra {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importer brukere fra {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importer brukere fra Plex",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {ny bruker} other {nye brukere}} ble importert fra Plex!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {ny bruker} other {nye brukere}} ble importert fra Plex!",
|
||||||
"components.Settings.menuUsers": "Brukere",
|
"components.Settings.menuUsers": "Brukere",
|
||||||
"components.Settings.SettingsUsers.users": "Brukere",
|
"components.Settings.SettingsUsers.users": "Brukere",
|
||||||
|
|||||||
@@ -214,7 +214,8 @@
|
|||||||
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
|
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker.",
|
||||||
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
|
"components.UserList.userdeleted": "Gebruiker succesvol verwijderd!",
|
||||||
"components.UserList.importfromplexerror": "Er is iets misgegaan bij het importeren van Plex-gebruikers.",
|
"components.UserList.importfromplexerror": "Er is iets misgegaan bij het importeren van Plex-gebruikers.",
|
||||||
"components.UserList.importfromplex": "{mediaServerName}-gebruikers importeren",
|
"components.UserList.importfrommediaserver": "{mediaServerName}-gebruikers importeren",
|
||||||
|
"components.UserList.importfromplex": "Plex-gebruikers importeren",
|
||||||
"components.UserList.deleteuser": "Gebruiker verwijderen",
|
"components.UserList.deleteuser": "Gebruiker verwijderen",
|
||||||
"components.UserList.deleteconfirm": "Weet je zeker dat je deze gebruiker wilt verwijderen? Al hun bestaande aanvraaggegevens zullen worden verwijderd.",
|
"components.UserList.deleteconfirm": "Weet je zeker dat je deze gebruiker wilt verwijderen? Al hun bestaande aanvraaggegevens zullen worden verwijderd.",
|
||||||
"components.TvDetails.watchtrailer": "Trailer bekijken",
|
"components.TvDetails.watchtrailer": "Trailer bekijken",
|
||||||
|
|||||||
@@ -962,7 +962,8 @@
|
|||||||
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Wyślij po cichu",
|
"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.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.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "<FindDiscordIdLink>Wielocyfrowy numer ID</FindDiscordIdLink> powiązany z Twoim kontem użytkownika",
|
||||||
"components.UserList.importfromplex": "Importuj użytkowników {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importuj użytkowników {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importuj użytkowników Plex",
|
||||||
"i18n.available": "Dostępny",
|
"i18n.available": "Dostępny",
|
||||||
"components.UserList.sortDisplayName": "Wyświetlana nazwa",
|
"components.UserList.sortDisplayName": "Wyświetlana nazwa",
|
||||||
"components.UserList.totalrequests": "Prośby",
|
"components.UserList.totalrequests": "Prośby",
|
||||||
|
|||||||
@@ -228,7 +228,8 @@
|
|||||||
"components.MovieDetails.viewfullcrew": "Ver Equipe Técnica Completa",
|
"components.MovieDetails.viewfullcrew": "Ver Equipe Técnica Completa",
|
||||||
"components.MovieDetails.MovieCrew.fullcrew": "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.importfromplexerror": "Algo deu errado ao importar usuários do Plex.",
|
||||||
"components.UserList.importfromplex": "Importar Usuários do {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importar Usuários do {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importar Usuários do Plex",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> {userCount, plural, one {usuário Plex importado} other {usuários Plex importados}} com sucesso!",
|
"components.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.Settings.Notifications.NotificationsSlack.agentenabled": "Habilitar Agente",
|
||||||
"components.RequestList.RequestItem.failedretry": "Algo deu errado ao retentar fazer a solicitação.",
|
"components.RequestList.RequestItem.failedretry": "Algo deu errado ao retentar fazer a solicitação.",
|
||||||
|
|||||||
@@ -199,7 +199,8 @@
|
|||||||
"components.UserList.passwordinfodescription": "Configurar um URL de aplicação e ativar as notificações por e-mail para permitir a geração automática de palavra-passe.",
|
"components.UserList.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.localuser": "Utilizador Local",
|
||||||
"components.UserList.importfromplexerror": "Ocorreu um erro ao importar utilizadores do Plex.",
|
"components.UserList.importfromplexerror": "Ocorreu um erro ao importar utilizadores do Plex.",
|
||||||
"components.UserList.importfromplex": "Importar Utilizadores do {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importar Utilizadores do {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importar Utilizadores do Plex",
|
||||||
"components.UserList.importedfromplex": "{userCount, plural, one {# novo utilizador} other {# novos utilizadores}} importados do Plex com sucesso!",
|
"components.UserList.importedfromplex": "{userCount, plural, one {# novo utilizador} other {# novos utilizadores}} importados do Plex com sucesso!",
|
||||||
"components.UserList.email": "Endereço de E-mail",
|
"components.UserList.email": "Endereço de E-mail",
|
||||||
"components.UserList.deleteuser": "Apagar Utilizador",
|
"components.UserList.deleteuser": "Apagar Utilizador",
|
||||||
|
|||||||
@@ -793,7 +793,8 @@
|
|||||||
"components.UserList.usercreatedfailed": "Что-то пошло не так при создании пользователя.",
|
"components.UserList.usercreatedfailed": "Что-то пошло не так при создании пользователя.",
|
||||||
"components.UserList.passwordinfodescription": "Настройте URL-адрес приложения и включите уведомления по электронной почте, чтобы обеспечить возможность автоматической генерации пароля.",
|
"components.UserList.passwordinfodescription": "Настройте URL-адрес приложения и включите уведомления по электронной почте, чтобы обеспечить возможность автоматической генерации пароля.",
|
||||||
"components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.",
|
"components.UserList.importfromplexerror": "Что-то пошло не так при импорте пользователей из Plex.",
|
||||||
"components.UserList.importfromplex": "Импортировать пользователей из {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Импортировать пользователей из {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Импортировать пользователей из Plex",
|
||||||
"components.UserList.importedfromplex": "{userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!",
|
"components.UserList.importedfromplex": "{userCount, plural, one {# новый пользователь} other {# новых пользователя(ей)}} успешно импортированы из Plex!",
|
||||||
"components.UserList.edituser": "Изменить разрешения пользователя",
|
"components.UserList.edituser": "Изменить разрешения пользователя",
|
||||||
"components.UserList.displayName": "Отображаемое имя",
|
"components.UserList.displayName": "Отображаемое имя",
|
||||||
|
|||||||
@@ -1006,7 +1006,8 @@
|
|||||||
"components.UserProfile.UserSettings.UserPermissions.toastSettingsFailure": "Diçka shkoi keq duke ruajtur cilësimet.",
|
"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.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.TvDetails.episodeRuntimeMinutes": "{runtime} minuta",
|
||||||
"components.UserList.importfromplex": "Importoni përdoruesit {mediaServerName}",
|
"components.UserList.importfrommediaserver": "Importoni përdoruesit {mediaServerName}",
|
||||||
|
"components.UserList.importfromplex": "Importoni përdoruesit Plex",
|
||||||
"components.UserList.importfromplexerror": "Diçka shkoi keq duke importuar përdoruesit Plex.",
|
"components.UserList.importfromplexerror": "Diçka shkoi keq duke importuar përdoruesit Plex.",
|
||||||
"components.TvDetails.firstAirDate": "Data e parë e transmetimit",
|
"components.TvDetails.firstAirDate": "Data e parë e transmetimit",
|
||||||
"components.UserList.email": "Adresa email",
|
"components.UserList.email": "Adresa email",
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Versionsdata är för närvarande inte tillgänglig.",
|
"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.latestversion": "Senaste Versionen",
|
||||||
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
|
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell",
|
||||||
"components.UserList.importfromplex": "Importera {mediaServerName}användare",
|
"components.UserList.importfrommediaserver": "Importera {mediaServerName}användare",
|
||||||
|
"components.UserList.importfromplex": "Importera Plexanvändare",
|
||||||
"components.UserList.importfromplexerror": "Något gick fel när Plexanvändare importerades.",
|
"components.UserList.importfromplexerror": "Något gick fel när Plexanvändare importerades.",
|
||||||
"components.TvDetails.watchtrailer": "Kolla Trailer",
|
"components.TvDetails.watchtrailer": "Kolla Trailer",
|
||||||
"components.Settings.Notifications.allowselfsigned": "Tillåt Självsignerade Certifikat",
|
"components.Settings.Notifications.allowselfsigned": "Tillåt Självsignerade Certifikat",
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"components.UserList.localuser": "本地用户",
|
"components.UserList.localuser": "本地用户",
|
||||||
"components.UserList.localLoginDisabled": "<strong>允许本地登录</strong>的设置目前被禁用。",
|
"components.UserList.localLoginDisabled": "<strong>允许本地登录</strong>的设置目前被禁用。",
|
||||||
"components.UserList.importfromplexerror": "导入 Plex 用户时出错。",
|
"components.UserList.importfromplexerror": "导入 Plex 用户时出错。",
|
||||||
"components.UserList.importfromplex": "导入 {mediaServerName} 用户",
|
"components.UserList.importfrommediaserver": "导入 {mediaServerName} 用户",
|
||||||
|
"components.UserList.importfromplex": "导入 Plex 用户",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} 成功导入!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} 成功导入!",
|
||||||
"components.UserList.email": "电子邮件地址",
|
"components.UserList.email": "电子邮件地址",
|
||||||
"components.UserList.edituser": "编辑用户权限",
|
"components.UserList.edituser": "编辑用户权限",
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"components.Settings.radarrsettings": "Radarr 設定",
|
"components.Settings.radarrsettings": "Radarr 設定",
|
||||||
"components.Settings.menuPlexSettings": "Plex",
|
"components.Settings.menuPlexSettings": "Plex",
|
||||||
"components.UserList.importfromplexerror": "匯入 Plex 使用者時出了點問題。",
|
"components.UserList.importfromplexerror": "匯入 Plex 使用者時出了點問題。",
|
||||||
"components.UserList.importfromplex": "匯入 {mediaServerName} 使用者",
|
"components.UserList.importfrommediaserver": "匯入 {mediaServerName} 使用者",
|
||||||
|
"components.UserList.importfromplex": "匯入 Plex 使用者",
|
||||||
"components.UserList.importedfromplex": "匯入 <strong>{userCount}</strong> 個 Plex 使用者成功!",
|
"components.UserList.importedfromplex": "匯入 <strong>{userCount}</strong> 個 Plex 使用者成功!",
|
||||||
"components.UserList.localuser": "本地使用者",
|
"components.UserList.localuser": "本地使用者",
|
||||||
"components.UserList.creating": "創建中…",
|
"components.UserList.creating": "創建中…",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
important: true,
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
content: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -1622,6 +1622,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
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":
|
"@next/swc-android-arm64@12.1.0":
|
||||||
version "12.1.0"
|
version "12.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.0.tgz#865ba3a9afc204ff2bdeea49dd64d58705007a39"
|
||||||
@@ -4780,6 +4787,11 @@ email-templates@^8.0.10:
|
|||||||
nodemailer "^6.7.2"
|
nodemailer "^6.7.2"
|
||||||
preview-email "^3.0.5"
|
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:
|
emoji-regex@^10.0.0:
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.1.tgz#77180edb279b99510a21b79b19e1dc283d8f3991"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.1.tgz#77180edb279b99510a21b79b19e1dc283d8f3991"
|
||||||
|
|||||||
Reference in New Issue
Block a user