Compare commits

...

46 Commits

Author SHA1 Message Date
fallenbagel
6caa0456bf feat: use gravatar for jellyfin users' with missing jellyfin avatars 2024-02-23 09:26:45 +05:00
fallenbagel
d9da3a2e3f refactor: jellyfin authentication
This refactor standardizes the authentication approach in Jellyfin to mirror the method employed in
Plex authentication for consistency
2024-02-23 09:18:13 +05:00
InvalidArgumentException
db84f6529a fix(jellyfin.ts): process virtual seasons if they have non virtual episodes (#639)
All seasons are processed now, but those without any episodes are filtered out again as unavailable.
This fixes in issue where jellyfin reports all seasons as virtual
2024-02-01 16:10:06 +05:00
Fallenbagel
4f81788386 Merge pull request #640 from Fallenbagel/all-contributors/add-Danish-H
docs: add Danish-H as a contributor for code
2024-01-29 00:54:02 +05:00
allcontributors[bot]
72d3f9b908 docs: update .all-contributorsrc [skip ci] 2024-01-28 19:53:51 +00:00
allcontributors[bot]
333ffed7f0 docs: update README.md [skip ci] 2024-01-28 19:53:50 +00:00
Fallenbagel
8641a26771 Merge pull request #636 from Danish-H/feature-letterboxd-links
feat: added Letterboxd links for the external link blocks for movies
2024-01-29 00:53:29 +05:00
Fallenbagel
7329524868 Merge pull request #638 from Fallenbagel/all-contributors/add-aleksasiriski
docs: add aleksasiriski as a contributor for infra
2024-01-28 01:10:56 +05:00
allcontributors[bot]
908dcb487a docs: update .all-contributorsrc [skip ci] 2024-01-27 20:10:44 +00:00
allcontributors[bot]
d486d58d3d docs: update README.md [skip ci] 2024-01-27 20:10:43 +00:00
Fallenbagel
d8b08f4c6b Merge pull request #637 from aleksasiriski/patch-1
ci(preview): added arm support for preview tags
2024-01-28 00:50:23 +05:00
Aleksa Siriški
a48a337e0f ci(preview): added arm support for preview tags 2024-01-27 16:58:35 +01:00
Danish Humair
981f5e679c feat: added Letterboxd links for the external link blocks for movies 2024-01-27 03:25:03 +05:00
Fallenbagel
7af193b8f6 docs: fix weblate link 2024-01-13 22:05:03 +05:00
Fallenbagel
6040e16645 update discord badge 2024-01-04 02:02:16 +05:00
Fallenbagel
3877301fc8 add translation percentage badge 2024-01-04 02:00:49 +05:00
Fallenbagel
092a1458a4 move weblate details to contributing.md 2024-01-03 14:25:23 +05:00
Fallenbagel
1c68111b12 update weblate link 2024-01-03 14:24:45 +05:00
Fallenbagel
0e777ddb1e Merge pull request #612 from Fallenbagel/feat-readme-weblate
Add more badges and weblate status
2024-01-03 14:20:29 +05:00
Fallenbagel
52c689b080 Merge pull request #613 from Fallenbagel/all-contributors/add-xeruf
docs: add xeruf as a contributor for doc
2024-01-03 14:12:32 +05:00
allcontributors[bot]
1a11f085ba docs: update .all-contributorsrc [skip ci] 2024-01-03 09:12:19 +00:00
allcontributors[bot]
c0234582a6 docs: update README.md [skip ci] 2024-01-03 09:12:18 +00:00
Fallenbagel
fd958d6347 Merge pull request #611 from xeruf/patch-1
Link related projects in README.md
2024-01-03 14:10:17 +05:00
Fallenbagel
6586db52dc Add more badges and weblate status 2024-01-03 14:04:17 +05:00
Janek
a41cb8b004 Link related projects in README.md 2024-01-03 07:39:48 +01:00
Fallenbagel
de66222e7a Merge pull request #590 from Fallenbagel/all-contributors/add-mdll23
docs: add mdll23 as a contributor for translation
2023-12-03 21:04:28 +05:00
allcontributors[bot]
eb790cb466 docs: update .all-contributorsrc [skip ci] 2023-12-03 16:03:26 +00:00
allcontributors[bot]
0680931332 docs: update README.md [skip ci] 2023-12-03 16:03:26 +00:00
Fallenbagel
ff2821471e Merge pull request #589 from mdll23/develop
fix: translation de.json
2023-12-03 21:02:59 +05:00
mdll23
e032c02f5f fix: fix german translation for "components.Discover.FilterSlideover.tmdbuservotecount" 2023-12-03 15:13:19 +01:00
Fallenbagel
f8c4def229 Merge pull request #565 from notquitenothing/custom-jellyfin-password-reset
feat: Custom jellyfin password reset setting
2023-11-30 14:08:20 +05:00
fallenbagel
a0415e7b6b Merge branch 'develop' into custom-jellyfin-password-reset 2023-11-30 09:26:14 +05:00
fallenbagel
b5f672785a docs: reverted two unrelated files to its develop branch state 2023-11-30 09:25:34 +05:00
Fallenbagel
770d788fd7 Merge pull request #577 from Fallenbagel/fix-set-defaultValue-seasonFoldersInSonarr
fix: disable seasonfolder option in sonarr for jellyfin/Emby users
2023-11-30 09:04:46 +05:00
Fallenbagel
c58261c841 Merge pull request #578 from Fallenbagel/refactor-jellyfin-scan-job
refactor: jellyfin scan jobs moved from server/jobs to server/libs/scanners
2023-11-27 15:05:02 +05:00
fallenbagel
ccfcdea1f6 refactor: clean out commented code 2023-11-27 14:11:10 +05:00
fallenbagel
8ec8f2ac57 fix: disable seasonfolder option in sonarr for jellyfin/Emby users
This disables seasonfolder option in sonarr for jellyfin/emby users as physical seasonFolders are
necessary as virtualFolders are ignored since #126

fix #575
2023-11-27 13:58:46 +05:00
fallenbagel
91f97f96ab refactor: jellyfin scan jobs moved from server/jobs to server/libs/scanners 2023-11-27 11:09:45 +05:00
Fallenbagel
f4051a1e5d Merge pull request #572 from Fallenbagel/fix-filterSlideOver-datepicker
fix: correct width issue in datepicker of filterSliderOver
2023-11-19 18:47:49 +05:00
fallenbagel
f564cddff4 fix: correct width issue in datepicker of filterSliderOver
This commit addresses a rendering issue with the date picker component.
The problem was traced back to a misconfiguration in the tailwindcss settings, resulting in an
incorrect width for the popup.

fix #415
2023-11-19 18:08:06 +05:00
Fallenbagel
cfcce6acf0 Merge pull request #571 from Fallenbagel/fix-local-watchlist-page-item-removal
fix: ensure watchlist updates are immediately reflected
2023-11-19 16:47:45 +05:00
fallenbagel
b85d7f37b9 fix: ensure watchlist updates are immediately reflected
This fix addresses an issue on the Watchlist page where changes to the watchlist were not
immediately reflected. Previously, after removing an item from the watchlist, the update
required a full page reload or revalidating upon focusing the window or tab. With this fix,
the watchlist now correctly mutates and updates in real-time, providing a seamless user
experience.
2023-11-19 16:33:02 +05:00
Fallenbagel
97396c2f57 Merge pull request #568 from Fallenbagel/fix-local-watchlist-page-item-removal
fix(watchlist): added missing prop for watchlist item removal button in watchlist page
2023-11-18 06:29:25 +05:00
Derek Paschal
0dfe050ba1 Fixing code formatting, prettier 2023-11-15 06:59:02 -06:00
Derek Paschal
13dd3cad54 Making the new setting optional 2023-11-14 08:51:29 -06:00
Derek Paschal
ce9802d5d4 Adding Jellyfin Setting for Custom "Forgot Password" URL
Adding Jellyfin Setting for Custom "Forgot Password" URL.  Useful in cases where you are using a custom authentication provider such as the LDAP plugin, Authelia, lldap, or any other external auth scheme with its own password reset page.
2023-11-14 08:20:28 -06:00
26 changed files with 305 additions and 111 deletions

View File

@@ -277,6 +277,42 @@
"contributions": [
"doc"
]
},
{
"login": "mdll23",
"name": "Michael Dallinger",
"avatar_url": "https://avatars.githubusercontent.com/u/142844478?v=4",
"profile": "https://github.com/mdll23",
"contributions": [
"translation"
]
},
{
"login": "xeruf",
"name": "Janek",
"avatar_url": "https://avatars.githubusercontent.com/u/13354331?v=4",
"profile": "https://github.com/xeruf",
"contributions": [
"doc"
]
},
{
"login": "aleksasiriski",
"name": "Aleksa Siriški",
"avatar_url": "https://avatars.githubusercontent.com/u/31509435?v=4",
"profile": "https://aleksasiriski.dev",
"contributions": [
"infra"
]
},
{
"login": "Danish-H",
"name": "Danish Humair",
"avatar_url": "https://avatars.githubusercontent.com/u/121830048?v=4",
"profile": "http://danishhumair.com",
"contributions": [
"code"
]
}
]
}

View File

@@ -29,7 +29,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}

View File

@@ -1,4 +1,4 @@
# Contributing to Overseerr
# Contributing to Jellyseerr
All help is welcome and greatly appreciated! If you would like to contribute to the project, the following instructions should get you started...
@@ -17,7 +17,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository to your own GitHub account and [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device:
```bash
git clone https://github.com/YOUR_USERNAME/overseerr.git
git clone https://github.com/YOUR_USERNAME/jellyseerr.git
cd overseerr/
```
@@ -97,9 +97,9 @@ When adding new UI text, please try to adhere to the following guidelines:
## Translation
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
<a href="https://hosted.weblate.org/engage/overseerr/"><img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/multi-auto.svg" alt="Translation status" /></a>
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
## Attribution

View File

@@ -2,23 +2,28 @@
<img src="./public/logo_full.svg" alt="Jellyseerr" style="margin: 20px 0;">
</p>
<p align="center">
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
<img src="https://github.com/Fallenbagel/jellyseerr/actions/workflows/release.yml/badge.svg" alt="Jellyseerr Release" />
<img src="https://github.com/Fallenbagel/jellyseerr/actions/workflows/ci.yml/badge.svg" alt="Jellyseerr CI">
</p>
<p align="center">
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" alt="Discord"></a>
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-29-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-33-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
**Jellyseerr** is a free and open source software application for managing requests for your media library.
It is a a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
## Current Features
- Full Jellyfin/Emby/Plex integration. Authenticate and manage user access with Jellyfin/Emby/Plex!
- Supports Movies, Shows, Mixed Libraries!
- Full Jellyfin/Emby/Plex integration including authentication with user import & management
- Supports Movies, Shows and Mixed Libraries
- Ability to change email addresses for smtp purposes
- Ability to import all jellyfin/emby users
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
@@ -49,7 +54,7 @@ https://hub.docker.com/r/fallenbagel/jellyseerr
Pre-requisites:
- Nodejs [v18](https://nodejs.org/download/release/v18.18.2)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install)
- Download/git clone the source code from the github (Either develop branch or main for stable)
```cmd
@@ -59,6 +64,7 @@ yarn install --frozen-lockfile --network-timeout 1000000
yarn run build
yarn start
```
(you can use task scheduler to run a bat script with `@echo off` and `yarn start` to run jellyseerr in the background)
_to set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` in the root directory of jellyseerr_
@@ -136,6 +142,7 @@ ExecStart=/usr/bin/node dist/index.js
[Install]
WantedBy=multi-user.target
```
### Packages:
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
@@ -217,6 +224,10 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -368,6 +368,9 @@ components:
externalHostname:
type: string
example: 'http://my.jellyfin.host'
jellyfinForgotPasswordUrl:
type: string
example: 'http://my.jellyfin.host/web/index.html#!/forgotpassword.html'
adminUser:
type: string
example: 'admin'

View File

@@ -261,9 +261,7 @@ class JellyfinAPI {
try {
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
return contents.data.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
);
return contents.data.Items;
} catch (e) {
logger.error(
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,

View File

@@ -24,6 +24,7 @@ export interface PublicSettingsResponse {
jellyfinHost?: string;
jellyfinExternalHost?: string;
jellyfinServerName?: string;
jellyfinForgotPasswordUrl?: string;
initialized: boolean;
applicationTitle: string;
applicationUrl: string;

View File

@@ -1,6 +1,10 @@
import { MediaServerType } from '@server/constants/server';
import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy';
import {
jellyfinFullScanner,
jellyfinRecentScanner,
} from '@server/lib/scanners/jellyfin';
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
import { radarrScanner } from '@server/lib/scanners/radarr';
import { sonarrScanner } from '@server/lib/scanners/sonarr';
@@ -10,7 +14,6 @@ import watchlistSync from '@server/lib/watchlistsync';
import logger from '@server/logger';
import random from 'lodash/random';
import schedule from 'node-schedule';
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
interface ScheduledJob {
id: JobId;
@@ -73,38 +76,38 @@ export const startJobs = (): void => {
// Run recently added jellyfin sync every 5 minutes
scheduledJobs.push({
id: 'jellyfin-recently-added-scan',
name: 'Jellyfin Recently Added Sync',
name: 'Jellyfin Recently Added Scan',
type: 'process',
interval: 'minutes',
cronSchedule: jobs['jellyfin-recently-added-scan'].schedule,
job: schedule.scheduleJob(
jobs['jellyfin-recently-added-scan'].schedule,
() => {
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
logger.info('Starting scheduled job: Jellyfin Recently Added Scan', {
label: 'Jobs',
});
jobJellyfinRecentSync.run();
jellyfinRecentScanner.run();
}
),
running: () => jobJellyfinRecentSync.status().running,
cancelFn: () => jobJellyfinRecentSync.cancel(),
running: () => jellyfinRecentScanner.status().running,
cancelFn: () => jellyfinRecentScanner.cancel(),
});
// Run full jellyfin sync every 24 hours
scheduledJobs.push({
id: 'jellyfin-full-scan',
name: 'Jellyfin Full Library Sync',
name: 'Jellyfin Full Library Scan',
type: 'process',
interval: 'hours',
cronSchedule: jobs['jellyfin-full-scan'].schedule,
job: schedule.scheduleJob(jobs['jellyfin-full-scan'].schedule, () => {
logger.info('Starting scheduled job: Jellyfin Full Sync', {
logger.info('Starting scheduled job: Jellyfin Full Scan', {
label: 'Jobs',
});
jobJellyfinFullSync.run();
jellyfinFullScanner.run();
}),
running: () => jobJellyfinFullSync.status().running,
cancelFn: () => jobJellyfinFullSync.cancel(),
running: () => jellyfinFullScanner.status().running,
cancelFn: () => jellyfinFullScanner.cancel(),
});
}

View File

@@ -26,7 +26,7 @@ interface SyncStatus {
libraries: Library[];
}
class JobJellyfinSync {
class JellyfinScanner {
private sessionId: string;
private tmdb: TheMovieDb;
private jfClient: JellyfinAPI;
@@ -675,7 +675,7 @@ class JobJellyfinSync {
}
}
export const jobJellyfinFullSync = new JobJellyfinSync();
export const jobJellyfinRecentSync = new JobJellyfinSync({
export const jellyfinFullScanner = new JellyfinScanner();
export const jellyfinRecentScanner = new JellyfinScanner({
isRecentOnly: true,
});

View File

@@ -40,6 +40,7 @@ export interface JellyfinSettings {
name: string;
hostname: string;
externalHostname?: string;
jellyfinForgotPasswordUrl?: string;
libraries: Library[];
serverId: string;
}
@@ -131,6 +132,7 @@ interface FullPublicSettings extends PublicSettings {
mediaServerType: number;
jellyfinHost?: string;
jellyfinExternalHost?: string;
jellyfinForgotPasswordUrl?: string;
jellyfinServerName?: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
@@ -331,6 +333,7 @@ class Settings {
name: '',
hostname: '',
externalHostname: '',
jellyfinForgotPasswordUrl: '',
libraries: [],
serverId: '',
},
@@ -534,6 +537,7 @@ class Settings {
applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable,
localLogin: this.data.main.localLogin,
jellyfinForgotPasswordUrl: this.data.jellyfin.jellyfinForgotPasswordUrl,
movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault
),

View File

@@ -11,6 +11,7 @@ import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
import * as EmailValidator from 'email-validator';
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
const authRoutes = Router();
@@ -274,24 +275,82 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
where: { jellyfinUserId: account.User.Id },
});
if (user) {
if (!user && !(await userRepository.count())) {
logger.info(
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
// User doesn't exist, and there are no users in the database, we'll create the user
// with admin permission
settings.main.mediaServerType = MediaServerType.JELLYFIN;
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
userType: UserType.JELLYFIN,
});
settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId;
settings.save();
startJobs();
await userRepository.save(user);
}
// User already exists, let's update their information
else if (body.username === user?.jellyfinUsername) {
logger.info(
`Found matching ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
} user; updating user with ${
settings.main.mediaServerType === MediaServerType.JELLYFIN
? 'Jellyfin'
: 'Emby'
}`,
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
// Let's check if their authtoken is up to date
if (user.jellyfinAuthToken !== account.AccessToken) {
user.jellyfinAuthToken = account.AccessToken;
}
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else {
user.avatar = '/os_logo_square.png';
user.avatar = gravatarUrl(user.email, {
default: 'mm',
size: 200,
});
}
user.jellyfinUsername = account.User.Name;
if (user.username === account.User.Name) {
user.username = '';
}
// If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY
if (process.env.JELLYFIN_TYPE === 'emby') {
settings.main.mediaServerType = MediaServerType.EMBY;
settings.save();
}
await userRepository.save(user);
} else if (!settings.main.newPlexLogin) {
logger.warn(
@@ -307,69 +366,38 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
status: 403,
message: 'Access denied.',
});
} else {
// Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions
const totalUsers = await userRepository.count();
if (totalUsers === 0) {
logger.info(
'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr',
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
}
);
user = new User({
email: body.email,
} else if (!user) {
logger.info(
'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user',
{
label: 'API',
ip: req.ip,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
await userRepository.save(user);
//Update hostname in settings if it doesn't exist (initial configuration)
//Also set mediaservertype to JELLYFIN
if (settings.jellyfin.hostname === '') {
settings.main.mediaServerType = MediaServerType.JELLYFIN;
settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId;
settings.save();
startJobs();
}
);
if (!body.email) {
throw new Error('add_email');
}
if (!user) {
if (!body.email) {
throw new Error('add_email');
}
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
//initialize Jellyfin/Emby users with local login
const passedExplicitPassword =
body.password && body.password.length > 0;
if (passedExplicitPassword) {
await user.setPassword(body.password ?? '');
}
await userRepository.save(user);
user = new User({
email: body.email,
jellyfinUsername: account.User.Name,
jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
userType: UserType.JELLYFIN,
});
//initialize Jellyfin/Emby users with local login
const passedExplicitPassword = body.password && body.password.length > 0;
if (passedExplicitPassword) {
await user.setPassword(body.password ?? '');
}
await userRepository.save(user);
}
// Set logged in session
@@ -400,6 +428,11 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
status: 406,
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
});
} else if (e.message === 'select_server_type') {
return next({
status: 406,
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
});
} else {
logger.error(e.message, { label: 'Auth' });
return next({

View File

@@ -12,12 +12,12 @@ import type {
LogsResultsResponse,
SettingsAboutResponse,
} from '@server/interfaces/api/settingsInterfaces';
import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
import { scheduledJobs } from '@server/job/schedule';
import type { AvailableCacheIds } from '@server/lib/cache';
import cacheManager from '@server/lib/cache';
import ImageProxy from '@server/lib/imageproxy';
import { Permission } from '@server/lib/permissions';
import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin';
import { plexFullScanner } from '@server/lib/scanners/plex';
import type { JobId, Library, MainSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
@@ -29,6 +29,7 @@ import { getAppVersion } from '@server/utils/appVersion';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
import gravatarUrl from 'gravatar-url';
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
import { rescheduleJob } from 'node-schedule';
import path from 'path';
@@ -337,7 +338,7 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
id: user.Id,
thumb: user.PrimaryImageTag
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
: gravatarUrl(user.Name, { default: 'mm', size: 200 }),
email: user.Name,
}));
@@ -345,16 +346,16 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
});
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
return res.status(200).json(jobJellyfinFullSync.status());
return res.status(200).json(jellyfinFullScanner.status());
});
settingsRoutes.post('/jellyfin/sync', (req, res) => {
if (req.body.cancel) {
jobJellyfinFullSync.cancel();
jellyfinFullScanner.cancel();
} else if (req.body.start) {
jobJellyfinFullSync.run();
jellyfinFullScanner.run();
}
return res.status(200).json(jobJellyfinFullSync.status());
return res.status(200).json(jellyfinFullScanner.status());
});
settingsRoutes.get('/tautulli', (_req, res) => {
const settings = getSettings();

View File

@@ -537,7 +537,10 @@ router.post(
permissions: settings.main.defaultPermissions,
avatar: jellyfinUser?.PrimaryImageTag
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
: '/os_logo_square.png',
: gravatarUrl(jellyfinUser?.Name ?? '', {
default: 'mm',
size: 200,
}),
userType: UserType.JELLYFIN,
});

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -19,6 +19,7 @@ type ListViewProps = {
isLoading?: boolean;
isReachingEnd?: boolean;
onScrollBottom: () => void;
mutateParent?: () => void;
};
const ListView = ({
@@ -28,6 +29,7 @@ const ListView = ({
onScrollBottom,
isReachingEnd,
plexItems,
mutateParent,
}: ListViewProps) => {
const intl = useIntl();
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
@@ -48,6 +50,7 @@ const ListView = ({
type={title.mediaType}
isAddedToWatchlist={true}
canExpand
mutateParent={mutateParent}
/>
</li>
);

View File

@@ -30,6 +30,7 @@ const DiscoverWatchlist = () => {
titles,
fetchMore,
error,
mutate,
} = useDiscover<WatchlistItem>(
`/api/v1/${
router.pathname.startsWith('/profile')
@@ -76,6 +77,7 @@ const DiscoverWatchlist = () => {
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
mutateParent={mutate}
/>
</>
);

View File

@@ -1,6 +1,7 @@
import EmbyLogo from '@app/assets/services/emby.svg';
import ImdbLogo from '@app/assets/services/imdb.svg';
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
import LetterboxdLogo from '@app/assets/services/letterboxd.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import RTLogo from '@app/assets/services/rt.svg';
import TmdbLogo from '@app/assets/services/tmdb.svg';
@@ -103,6 +104,16 @@ const ExternalLinkBlock = ({
<TraktLogo />
</a>
)}
{tmdbId && mediaType === MediaType.MOVIE && (
<a
href={`https://letterboxd.com/tmdb/${tmdbId}`}
className="w-8 opacity-50 transition duration-300 hover:opacity-100"
target="_blank"
rel="noreferrer"
>
<LetterboxdLogo />
</a>
)}
</div>
);
};

View File

@@ -222,6 +222,8 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
const baseUrl = settings.currentSettings.jellyfinExternalHost
? settings.currentSettings.jellyfinExternalHost
: settings.currentSettings.jellyfinHost;
const jellyfinForgotPasswordUrl =
settings.currentSettings.jellyfinForgotPasswordUrl;
return (
<div>
<Formik
@@ -298,11 +300,15 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
<Button
as="a"
buttonType="ghost"
href={`${baseUrl}/web/index.html#!/${
process.env.JELLYFIN_TYPE === 'emby'
? 'startup/'
: ''
}forgotpassword.html`}
href={
jellyfinForgotPasswordUrl
? `${jellyfinForgotPasswordUrl}`
: `${baseUrl}/web/index.html#!/${
process.env.JELLYFIN_TYPE === 'emby'
? 'startup/'
: ''
}forgotpassword.html`
}
>
{intl.formatMessage(messages.forgotpassword)}
</Button>

View File

@@ -30,9 +30,10 @@ const messages = defineMessages({
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
jellyfinSettings: '{mediaServerName} Settings',
jellyfinSettingsDescription:
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL.',
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.',
externalUrl: 'External URL',
internalUrl: 'Internal URL',
jellyfinForgotPasswordUrl: 'Forgot Password URL',
validationUrl: 'You must provide a valid URL',
syncing: 'Syncing',
syncJellyfin: 'Sync Libraries',
@@ -94,6 +95,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
intl.formatMessage(messages.validationUrl)
),
jellyfinForgotPasswordUrl: Yup.string().matches(
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
intl.formatMessage(messages.validationUrl)
),
});
const activeLibraries =
@@ -353,6 +358,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
initialValues={{
jellyfinInternalUrl: data?.hostname || '',
jellyfinExternalUrl: data?.externalHostname || '',
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
}}
validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => {
@@ -360,6 +366,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
await axios.post('/api/v1/settings/jellyfin', {
hostname: values.jellyfinInternalUrl,
externalHostname: values.jellyfinExternalUrl,
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
} as JellyfinSettings);
addToast(
@@ -437,6 +444,30 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="jellyfinForgotPasswordUrl"
className="text-label"
>
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
type="text"
inputMode="url"
id="jellyfinForgotPasswordUrl"
name="jellyfinForgotPasswordUrl"
/>
</div>
{errors.jellyfinForgotPasswordUrl &&
touched.jellyfinForgotPasswordUrl && (
<div className="error">
{errors.jellyfinForgotPasswordUrl}
</div>
)}
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -1,8 +1,10 @@
import Modal from '@app/components/Common/Modal';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import type { SonarrSettings } from '@server/lib/settings';
import { MediaServerType } from '@server/constants/server';
import { type SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -109,6 +111,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
const [isTesting, setIsTesting] = useState(false);
const settings = useSettings();
const [testResponse, setTestResponse] = useState<TestResponse>({
profiles: [],
rootFolders: [],
@@ -255,7 +258,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
animeTags: sonarr?.animeTags ?? [],
isDefault: sonarr?.isDefault ?? false,
is4k: sonarr?.is4k ?? false,
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
enableSeasonFolders:
sonarr?.enableSeasonFolders ??
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
externalUrl: sonarr?.externalUrl,
syncEnabled: sonarr?.syncEnabled ?? false,
enableSearch: !sonarr?.preventSearch,
@@ -961,11 +966,24 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
>
{intl.formatMessage(messages.seasonfolders)}
</label>
<div className="form-input-area">
<div
className={`form-input-area ${
settings.currentSettings.mediaServerType ===
MediaServerType.JELLYFIN ||
settings.currentSettings.mediaServerType ===
MediaServerType.EMBY
? 'opacity-50'
: 'opacity-100'
}`}
>
<Field
type="checkbox"
id="enableSeasonFolders"
name="enableSeasonFolders"
disabled={
settings.currentSettings.mediaServerType !==
MediaServerType.PLEX
}
/>
</div>
</div>

View File

@@ -12,6 +12,7 @@ export interface TmdbTitleCardProps {
type: 'movie' | 'tv';
canExpand?: boolean;
isAddedToWatchlist?: boolean;
mutateParent?: () => void;
}
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -25,6 +26,7 @@ const TmdbTitleCard = ({
type,
canExpand,
isAddedToWatchlist = false,
mutateParent,
}: TmdbTitleCardProps) => {
const { hasPermission } = useUser();
@@ -71,6 +73,7 @@ const TmdbTitleCard = ({
year={title.releaseDate}
mediaType={'movie'}
canExpand={canExpand}
mutateParent={mutateParent}
/>
) : (
<TitleCard
@@ -87,6 +90,7 @@ const TmdbTitleCard = ({
year={title.firstAirDate}
mediaType={'tv'}
canExpand={canExpand}
mutateParent={mutateParent}
/>
);
};

View File

@@ -38,6 +38,7 @@ interface TitleCardProps {
canExpand?: boolean;
inProgress?: boolean;
isAddedToWatchlist?: number | boolean;
mutateParent?: () => void;
}
const messages = defineMessages({
@@ -61,6 +62,7 @@ const TitleCard = ({
isAddedToWatchlist = false,
inProgress = false,
canExpand = false,
mutateParent,
}: TitleCardProps) => {
const isTouch = useIsTouch();
const intl = useIntl();
@@ -148,6 +150,9 @@ const TitleCard = ({
} finally {
setIsUpdating(false);
mutate('/api/v1/discover/watchlist');
if (mutateParent) {
mutateParent();
}
setToggleWatchlist((prevState) => !prevState);
}
};

View File

@@ -25,6 +25,7 @@ interface DiscoverResult<T, S> {
error: unknown;
titles: T[];
firstResultData?: BaseSearchResult<T> & S;
mutate?: () => void;
}
const extraEncodes: [RegExp, string][] = [
@@ -54,7 +55,7 @@ const useDiscover = <
{ hideAvailable = true } = {}
): DiscoverResult<T, S> => {
const settings = useSettings();
const { data, error, size, setSize, isValidating } = useSWRInfinite<
const { data, error, size, setSize, isValidating, mutate } = useSWRInfinite<
BaseSearchResult<T> & S
>(
(pageIndex: number, previousPageData) => {
@@ -119,6 +120,7 @@ const useDiscover = <
error,
titles,
firstResultData: data?.[0],
mutate,
};
};

View File

@@ -1235,7 +1235,7 @@
"components.Discover.tmdbmoviestreamingservices": "TMDB Film-Streaming-Dienste",
"components.Discover.tmdbtvstreamingservices": "TMDB TV-Streaming-Dienste",
"i18n.collection": "Sammlung",
"components.Discover.FilterSlideover.tmdbuservotecount": "TMDB Kullanıcı Oy Sayısı",
"components.Discover.FilterSlideover.tmdbuservotecount": "Anzahl an TMDB Benutzerbewertungen",
"components.Settings.RadarrModal.tagRequestsInfo": "Füge automatisch ein Tag hinzu mit der ID und dem Namen des anfordernden Nutzers",
"components.MovieDetails.imdbuserscore": "IMDB Nutzer Bewertung",
"components.Settings.SonarrModal.tagRequests": "Tag Anforderungen",

View File

@@ -938,7 +938,7 @@
"components.Settings.internalUrl": "Internal URL",
"components.Settings.is4k": "4K",
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL.",
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",

View File

@@ -3,7 +3,6 @@ const defaultTheme = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
module.exports = {
important: true,
mode: 'jit',
content: [
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',